@tishlang/tish 1.0.29 → 1.0.33

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 (60) hide show
  1. package/Cargo.toml +1 -0
  2. package/crates/js_to_tish/src/transform/expr.rs +15 -6
  3. package/crates/tish/Cargo.toml +1 -1
  4. package/crates/tish/src/main.rs +1 -1
  5. package/crates/tish/tests/integration_test.rs +4 -3
  6. package/crates/tish_ast/src/ast.rs +65 -2
  7. package/crates/tish_build_utils/src/lib.rs +10 -2
  8. package/crates/tish_builtins/src/construct.rs +177 -0
  9. package/crates/tish_builtins/src/globals.rs +3 -5
  10. package/crates/tish_builtins/src/helpers.rs +2 -3
  11. package/crates/tish_builtins/src/lib.rs +1 -0
  12. package/crates/tish_builtins/src/object.rs +3 -4
  13. package/crates/tish_bytecode/src/compiler.rs +85 -11
  14. package/crates/tish_bytecode/src/opcode.rs +7 -3
  15. package/crates/tish_compile/Cargo.toml +1 -0
  16. package/crates/tish_compile/src/codegen.rs +233 -71
  17. package/crates/tish_compile/src/lib.rs +35 -0
  18. package/crates/tish_compile_js/Cargo.toml +1 -0
  19. package/crates/tish_compile_js/src/codegen.rs +38 -94
  20. package/crates/tish_compile_js/src/lib.rs +0 -1
  21. package/crates/tish_compile_js/src/tests_jsx.rs +68 -0
  22. package/crates/tish_core/Cargo.toml +4 -0
  23. package/crates/tish_core/src/console_style.rs +7 -1
  24. package/crates/tish_core/src/json.rs +1 -2
  25. package/crates/tish_core/src/macros.rs +2 -3
  26. package/crates/tish_core/src/value.rs +10 -5
  27. package/crates/tish_eval/Cargo.toml +2 -0
  28. package/crates/tish_eval/src/eval.rs +149 -72
  29. package/crates/tish_eval/src/http.rs +3 -4
  30. package/crates/tish_eval/src/regex.rs +3 -2
  31. package/crates/tish_eval/src/value.rs +11 -13
  32. package/crates/tish_eval/src/value_convert.rs +4 -8
  33. package/crates/tish_fmt/src/lib.rs +49 -10
  34. package/crates/tish_lexer/src/token.rs +2 -0
  35. package/crates/tish_lint/src/lib.rs +9 -0
  36. package/crates/tish_lsp/README.md +1 -1
  37. package/crates/tish_native/src/build.rs +16 -2
  38. package/crates/tish_opt/src/lib.rs +15 -0
  39. package/crates/tish_parser/src/lib.rs +101 -1
  40. package/crates/tish_parser/src/parser.rs +161 -50
  41. package/crates/tish_runtime/src/http.rs +4 -5
  42. package/crates/tish_runtime/src/http_fetch.rs +9 -10
  43. package/crates/tish_runtime/src/lib.rs +9 -2
  44. package/crates/tish_runtime/src/promise.rs +2 -3
  45. package/crates/tish_runtime/src/promise_io.rs +2 -3
  46. package/crates/tish_runtime/src/ws.rs +7 -7
  47. package/crates/tish_ui/Cargo.toml +17 -0
  48. package/crates/tish_ui/src/jsx.rs +390 -0
  49. package/crates/tish_ui/src/lib.rs +16 -0
  50. package/crates/tish_ui/src/runtime/hooks.rs +122 -0
  51. package/crates/tish_ui/src/runtime/mod.rs +173 -0
  52. package/crates/tish_vm/src/vm.rs +121 -27
  53. package/justfile +3 -3
  54. package/package.json +1 -1
  55. package/platform/darwin-arm64/tish +0 -0
  56. package/platform/darwin-x64/tish +0 -0
  57. package/platform/linux-arm64/tish +0 -0
  58. package/platform/linux-x64/tish +0 -0
  59. package/platform/win32-x64/tish.exe +0 -0
  60. package/crates/tish_compile_js/src/js_intrinsics.rs +0 -82
@@ -53,6 +53,41 @@ for (let i = 0; i < 5; i = i + 1) {
53
53
  );
54
54
  }
55
55
 
56
+ #[test]
57
+ fn new_expression_lowers_to_construct_on_native() {
58
+ let src = "fn f() { return new Uint8Array(4) }";
59
+ let program = parse(src).unwrap();
60
+ let rust = compile(&program).unwrap();
61
+ assert!(
62
+ rust.contains("tish_construct"),
63
+ "expected new to lower to tish_construct, got snippet missing it"
64
+ );
65
+ }
66
+
67
+ /// User-defined constructor name: `new ClassName(...)` must compile natively (host `construct`)
68
+ /// and is the same surface syntax as the JS target (`new` in emitted JavaScript).
69
+ #[test]
70
+ fn new_class_name_compiles_native_via_tish_construct() {
71
+ let src = r#"
72
+ fn ClassName(x) {
73
+ return x
74
+ }
75
+ fn factory() {
76
+ return new ClassName(42)
77
+ }
78
+ "#;
79
+ let program = parse(src).unwrap();
80
+ let rust = compile(&program).unwrap();
81
+ assert!(
82
+ rust.contains("tish_construct"),
83
+ "expected new ClassName to lower to tish_construct"
84
+ );
85
+ assert!(
86
+ rust.contains("ClassName"),
87
+ "expected emitted Rust to reference ClassName callable"
88
+ );
89
+ }
90
+
56
91
  #[test]
57
92
  fn loop_var_decl_clone_via_project_full() {
58
93
  let manifest = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
@@ -14,3 +14,4 @@ tishlang_ast = { path = "../tish_ast", version = ">=0.1" }
14
14
  tishlang_compile = { path = "../tish_compile", version = ">=0.1" }
15
15
  tishlang_opt = { path = "../tish_opt", version = ">=0.1" }
16
16
  tishlang_parser = { path = "../tish_parser", version = ">=0.1" }
17
+ tishlang_ui = { path = "../tish_ui", default-features = false, features = ["compiler"] }
@@ -2,18 +2,15 @@
2
2
 
3
3
  use tishlang_ast::{
4
4
  ArrayElement, ArrowBody, BinOp, CallArg, CompoundOp, DestructElement, DestructPattern, Expr,
5
- JsxAttrValue, JsxChild, JsxProp, Literal, LogicalAssignOp, MemberProp, ObjectProp, Program,
6
- Statement, UnaryOp,
5
+ FunParam, Literal, LogicalAssignOp, MemberProp, ObjectProp, Program, Statement, UnaryOp,
7
6
  };
8
7
 
9
8
  use crate::error::CompileError;
10
- use crate::js_intrinsics::{JsIntrinsic, JsIntrinsics};
11
9
 
12
10
  struct Codegen {
13
11
  output: String,
14
12
  indent: usize,
15
13
  in_async: bool,
16
- intrinsics: JsIntrinsics,
17
14
  }
18
15
 
19
16
  fn stmt_terminates_switch(stmt: Option<&Statement>) -> bool {
@@ -29,7 +26,6 @@ impl Codegen {
29
26
  output: String::new(),
30
27
  indent: 0,
31
28
  in_async: false,
32
- intrinsics: JsIntrinsics::new(),
33
29
  }
34
30
  }
35
31
 
@@ -61,9 +57,6 @@ impl Codegen {
61
57
  for stmt in &program.statements {
62
58
  self.emit_statement(stmt)?;
63
59
  }
64
- self.output = self
65
- .intrinsics
66
- .prepend_runtime_preamble(std::mem::take(&mut self.output));
67
60
  Ok(())
68
61
  }
69
62
 
@@ -304,20 +297,34 @@ impl Codegen {
304
297
 
305
298
  fn emit_params(
306
299
  &mut self,
307
- params: &[tishlang_ast::TypedParam],
300
+ params: &[FunParam],
308
301
  rest_param: Option<&tishlang_ast::TypedParam>,
309
302
  ) -> Result<String, CompileError> {
310
- let mut parts: Vec<String> = params
311
- .iter()
312
- .map(|p| {
313
- let n = Self::escape_ident(p.name.as_ref());
314
- if let Some(ref d) = p.default {
315
- format!("{} = {}", n, self.emit_expr(d).unwrap())
316
- } else {
317
- n
303
+ let mut parts: Vec<String> = Vec::new();
304
+ for p in params {
305
+ match p {
306
+ FunParam::Simple(tp) => {
307
+ let n = Self::escape_ident(tp.name.as_ref());
308
+ let s = if let Some(ref d) = tp.default {
309
+ format!("{} = {}", n, self.emit_expr(d)?)
310
+ } else {
311
+ n
312
+ };
313
+ parts.push(s);
314
+ }
315
+ FunParam::Destructure {
316
+ pattern,
317
+ type_ann: _,
318
+ default,
319
+ } => {
320
+ let mut s = self.emit_destruct_pattern(pattern)?;
321
+ if let Some(ref d) = default {
322
+ s = format!("{} = {}", s, self.emit_expr(d)?);
323
+ }
324
+ parts.push(s);
318
325
  }
319
- })
320
- .collect();
326
+ }
327
+ }
321
328
  if let Some(rest) = rest_param {
322
329
  parts.push(format!("...{}", Self::escape_ident(rest.name.as_ref())));
323
330
  }
@@ -421,16 +428,6 @@ impl Codegen {
421
428
  }
422
429
  }
423
430
  Expr::Call { callee, args, .. } => {
424
- if let Some(kind) =
425
- JsIntrinsics::classify_call(callee.as_ref(), args)?
426
- {
427
- self.intrinsics.mark(kind);
428
- if kind == JsIntrinsic::Uint8Array {
429
- let n = self.emit_call_arg(&args[0])?;
430
- return Ok(JsIntrinsics::emit_expr(kind, &n));
431
- }
432
- return Ok(JsIntrinsics::emit_expr(kind, ""));
433
- }
434
431
  let c = self.emit_expr(callee)?;
435
432
  let arg_strs: Result<Vec<_>, _> =
436
433
  args.iter().map(|a| self.emit_call_arg(a)).collect();
@@ -438,6 +435,13 @@ impl Codegen {
438
435
  // Tish uses null for undefined (e.g. empty array pop/shift)
439
436
  format!("({}({}) ?? null)", c, arg_strs)
440
437
  }
438
+ Expr::New { callee, args, .. } => {
439
+ let c = self.emit_expr(callee)?;
440
+ let arg_strs: Result<Vec<_>, _> =
441
+ args.iter().map(|a| self.emit_call_arg(a)).collect();
442
+ let arg_strs = arg_strs?.join(", ");
443
+ format!("(new {}({}) ?? null)", c, arg_strs)
444
+ }
441
445
  Expr::Member {
442
446
  object,
443
447
  prop,
@@ -625,23 +629,11 @@ impl Codegen {
625
629
  let o = self.emit_expr(operand)?;
626
630
  format!("(await {})", o)
627
631
  }
628
- Expr::JsxElement { tag, props, children, .. } => {
629
- let tag_str = if tag.chars().next().map(|c| c.is_uppercase()).unwrap_or(false) {
630
- tag.as_ref().to_string()
631
- } else {
632
- format!("{:?}", tag.as_ref())
633
- };
634
- let props_str = self.emit_jsx_props(props)?;
635
- let children_strs: Result<Vec<_>, _> =
636
- children.iter().map(|c| self.emit_jsx_child(c)).collect();
637
- let children_str = children_strs?.join(", ");
638
- format!("h({}, {}, [{}])", tag_str, props_str, children_str)
639
- }
640
- Expr::JsxFragment { children, .. } => {
641
- let children_strs: Result<Vec<_>, _> =
642
- children.iter().map(|c| self.emit_jsx_child(c)).collect();
643
- let children_str = children_strs?.join(", ");
644
- format!("h(Fragment, null, [{}])", children_str)
632
+ Expr::JsxElement { .. } | Expr::JsxFragment { .. } => {
633
+ tishlang_ui::jsx::emit_jsx_js(expr, &mut |e| {
634
+ self.emit_expr(e).map_err(|ce| ce.message)
635
+ })
636
+ .map_err(|m| CompileError { message: m })?
645
637
  }
646
638
  Expr::NativeModuleLoad { spec, .. } => {
647
639
  return Err(CompileError {
@@ -661,54 +653,6 @@ impl Codegen {
661
653
  }
662
654
  }
663
655
 
664
- fn emit_jsx_props(&mut self, props: &[JsxProp]) -> Result<String, CompileError> {
665
- if props.is_empty() {
666
- return Ok("null".to_string());
667
- }
668
- let parts: Result<Vec<_>, _> = props
669
- .iter()
670
- .map(|p| match p {
671
- JsxProp::Attr { name, value } => {
672
- let val = match value {
673
- JsxAttrValue::String(s) => format!("{:?}", s.as_ref()),
674
- JsxAttrValue::Expr(e) => self.emit_expr(e)?,
675
- JsxAttrValue::ImplicitTrue => "true".to_string(),
676
- };
677
- let key = name.as_ref();
678
- Ok(if key.chars().all(|c| c.is_alphanumeric() || c == '_') {
679
- format!("{}: {}", key, val)
680
- } else {
681
- format!("{:?}: {}", key, val)
682
- })
683
- }
684
- JsxProp::Spread(e) => Ok(format!("...{}", self.emit_expr(e)?)),
685
- })
686
- .collect();
687
- Ok(format!("{{ {} }}", parts?.join(", ")))
688
- }
689
-
690
- fn emit_jsx_child(&mut self, child: &JsxChild) -> Result<String, CompileError> {
691
- match child {
692
- JsxChild::Text(s) => Ok(format!("{:?}", s.as_ref())),
693
- JsxChild::Expr(e) => {
694
- let inner = self.emit_expr(e)?;
695
- // Only wrap literals we know are primitives (number, bool, null). Never wrap:
696
- // string/template (already strings), JSX (elements), Call (components), Array/Ident (may hold elements).
697
- let needs_string = matches!(
698
- e,
699
- Expr::Literal {
700
- value: Literal::Number(_) | Literal::Bool(_) | Literal::Null,
701
- ..
702
- }
703
- );
704
- Ok(if needs_string {
705
- format!("String({})", inner)
706
- } else {
707
- inner
708
- })
709
- }
710
- }
711
- }
712
656
  }
713
657
 
714
658
  /// Compile a single program (no imports) to JavaScript. JSX lowers to `h` / `Fragment` (Lattish).
@@ -3,7 +3,6 @@
3
3
 
4
4
  mod codegen;
5
5
  mod error;
6
- mod js_intrinsics;
7
6
 
8
7
  #[cfg(test)]
9
8
  mod tests_jsx;
@@ -211,4 +211,72 @@ fn FileList() {
211
211
  &js[..900.min(js.len())]
212
212
  );
213
213
  }
214
+
215
+ #[test]
216
+ fn new_date_global_emits_valid_js_with_and_without_optimize() {
217
+ let src = "let epoch = new Date(0)\nconsole.log(epoch.getTime())";
218
+ let program = parse(src).expect("parse");
219
+ for optimize in [false, true] {
220
+ let js = compile_with_jsx(&program, optimize).expect("compile");
221
+ assert!(
222
+ js.contains("new Date(0)"),
223
+ "optimize={optimize}: expected `new Date(0)` in JS output:\n{js}"
224
+ );
225
+ assert!(
226
+ !js.contains("let epoch = new;"),
227
+ "optimize={optimize}: broken `new` emission (missing constructor):\n{js}"
228
+ );
229
+ }
230
+ }
231
+
232
+ #[test]
233
+ fn new_uint8array_emits_direct_new_no_preamble() {
234
+ let src = "fn f(n) { return new Uint8Array(n) }";
235
+ let program = parse(src).expect("parse");
236
+ let js = compile_with_jsx(&program, false).expect("compile");
237
+ assert!(
238
+ js.contains("new Uint8Array("),
239
+ "expected direct new Uint8Array, got: {}",
240
+ &js[..500.min(js.len())]
241
+ );
242
+ assert!(
243
+ !js.contains("__tishUint8Array"),
244
+ "should not emit legacy intrinsic helper"
245
+ );
246
+ }
247
+
248
+ #[test]
249
+ fn new_audio_context_emits_direct_new_no_preamble() {
250
+ let src = "fn f() { return new AudioContext() }";
251
+ let program = parse(src).expect("parse");
252
+ let js = compile_with_jsx(&program, false).expect("compile");
253
+ assert!(
254
+ js.contains("new AudioContext("),
255
+ "expected new AudioContext, got: {}",
256
+ &js[..500.min(js.len())]
257
+ );
258
+ assert!(
259
+ !js.contains("__tishWebAudioCreateContext"),
260
+ "should not emit legacy intrinsic helper"
261
+ );
262
+ }
263
+
264
+ #[test]
265
+ fn new_class_name_emits_direct_new_js() {
266
+ let src = r#"
267
+ fn ClassName(x) {
268
+ return x
269
+ }
270
+ fn factory() {
271
+ return new ClassName(42)
272
+ }
273
+ "#;
274
+ let program = parse(src).expect("parse");
275
+ let js = compile_with_jsx(&program, false).expect("compile");
276
+ assert!(
277
+ js.contains("new ClassName("),
278
+ "expected new ClassName( in JS output, got: {}",
279
+ &js[..800.min(js.len())]
280
+ );
281
+ }
214
282
  }
@@ -11,4 +11,8 @@ default = []
11
11
  regex = ["dep:fancy-regex"]
12
12
 
13
13
  [dependencies]
14
+ ahash = "0.8.11"
14
15
  fancy-regex = { version = "0.17.0", optional = true }
16
+
17
+ [target.wasm32-unknown-unknown.dependencies]
18
+ getrandom = { version = "0.3", features = ["wasm_js"] }
@@ -4,9 +4,12 @@
4
4
  //! booleans, null, and object structure are easier to scan.
5
5
 
6
6
  use std::io::IsTerminal;
7
+ use std::sync::OnceLock;
7
8
 
8
9
  use crate::Value;
9
10
 
11
+ static CONSOLE_USES_COLORS: OnceLock<bool> = OnceLock::new();
12
+
10
13
  /// ANSI escape codes (standard 4-bit + bright black for dim).
11
14
  const RESET: &str = "\x1b[0m";
12
15
  /// Number: yellow (Node-style)
@@ -25,8 +28,11 @@ const PUNCT: &str = "\x1b[90m";
25
28
  const SPECIAL: &str = "\x1b[90m";
26
29
 
27
30
  /// Returns whether console output should use colors (stdout is a TTY).
31
+ ///
32
+ /// Cached for the process lifetime. `is_terminal()` is a syscall; benchmarks and
33
+ /// scripts with many `console.log` calls must not pay it on every line.
28
34
  pub fn use_console_colors() -> bool {
29
- std::io::stdout().is_terminal()
35
+ *CONSOLE_USES_COLORS.get_or_init(|| std::io::stdout().is_terminal())
30
36
  }
31
37
 
32
38
  /// Format a single value for console with optional ANSI colors (Node/Bun-style).
@@ -2,7 +2,6 @@
2
2
 
3
3
  use crate::Value;
4
4
  use std::cell::RefCell;
5
- use std::collections::HashMap;
6
5
  use std::rc::Rc;
7
6
  use std::sync::Arc;
8
7
 
@@ -260,7 +259,7 @@ fn parse_array(input: &str) -> Result<(Value, &str), String> {
260
259
 
261
260
  fn parse_object(input: &str) -> Result<(Value, &str), String> {
262
261
  let mut input = &input[1..]; // skip '{'
263
- let mut map = HashMap::new();
262
+ let mut map = crate::ObjectMap::default();
264
263
 
265
264
  input = input.trim_start();
266
265
  if let Some(rest) = input.strip_prefix('}') {
@@ -24,11 +24,10 @@
24
24
  macro_rules! tish_module {
25
25
  ($($name:expr => $fn:expr),* $(,)?) => {{
26
26
  use std::cell::RefCell;
27
- use std::collections::HashMap;
28
27
  use std::rc::Rc;
29
28
  use std::sync::Arc;
30
- use $crate::Value;
31
- let mut map = HashMap::new();
29
+ use $crate::{ObjectMap, Value};
30
+ let mut map = ObjectMap::default();
32
31
  $(
33
32
  map.insert(Arc::from($name), Value::Function(Rc::new($fn)));
34
33
  )*
@@ -1,10 +1,15 @@
1
1
  //! Unified Value type for Tish runtime values.
2
2
 
3
3
  use std::cell::RefCell;
4
- use std::collections::HashMap;
5
4
  use std::rc::Rc;
6
5
  use std::sync::Arc;
7
6
 
7
+ use ahash::AHashMap;
8
+
9
+ /// Property map for objects and other `Arc<str>` → `Value` tables (VM globals, scopes).
10
+ /// Uses a faster hasher than `std::collections::HashMap` for string-heavy workloads.
11
+ pub type ObjectMap = AHashMap<Arc<str>, Value>;
12
+
8
13
  #[cfg(feature = "regex")]
9
14
  use fancy_regex::Regex;
10
15
 
@@ -147,7 +152,7 @@ pub enum Value {
147
152
  Bool(bool),
148
153
  Null,
149
154
  Array(Rc<RefCell<Vec<Value>>>),
150
- Object(Rc<RefCell<HashMap<Arc<str>, Value>>>),
155
+ Object(Rc<RefCell<ObjectMap>>),
151
156
  Function(NativeFn),
152
157
  #[cfg(feature = "regex")]
153
158
  RegExp(Rc<RefCell<TishRegExp>>),
@@ -256,8 +261,8 @@ impl Value {
256
261
  Value::Array(Rc::new(RefCell::new(items)))
257
262
  }
258
263
 
259
- /// Create a new object Value from a HashMap.
260
- pub fn object(map: HashMap<Arc<str>, Value>) -> Self {
264
+ /// Create a new object Value from a property map.
265
+ pub fn object(map: ObjectMap) -> Self {
261
266
  Value::Object(Rc::new(RefCell::new(map)))
262
267
  }
263
268
 
@@ -268,7 +273,7 @@ impl Value {
268
273
 
269
274
  /// Create an empty object Value.
270
275
  pub fn empty_object() -> Self {
271
- Value::Object(Rc::new(RefCell::new(HashMap::new())))
276
+ Value::Object(Rc::new(RefCell::new(ObjectMap::default())))
272
277
  }
273
278
 
274
279
  /// Extract the number value, if this is a Number.
@@ -16,8 +16,10 @@ tokio = ["dep:tokio"]
16
16
  ws = ["dep:tishlang_runtime", "tishlang_runtime/ws"]
17
17
 
18
18
  [dependencies]
19
+ ahash = "0.8.12"
19
20
  rand = "0.10.0"
20
21
  tishlang_ast = { path = "../tish_ast", version = ">=0.1" }
22
+ tishlang_builtins = { path = "../tish_builtins", version = ">=0.1" }
21
23
  tishlang_parser = { path = "../tish_parser", version = ">=0.1" }
22
24
  tishlang_core = { path = "../tish_core", version = ">=0.1" }
23
25
  reqwest = { version = "0.13.2", default-features = false, features = ["rustls", "json", "stream"], optional = true }