@tishlang/tish 1.13.2 → 2.0.0

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 (106) hide show
  1. package/Cargo.toml +2 -0
  2. package/bin/tish +0 -0
  3. package/crates/js_to_tish/src/transform/expr.rs +1 -0
  4. package/crates/tish/Cargo.toml +11 -3
  5. package/crates/tish/build.rs +21 -0
  6. package/crates/tish/src/cli_help.rs +15 -4
  7. package/crates/tish/src/main.rs +93 -21
  8. package/crates/tish/src/repl_completion.rs +0 -1
  9. package/crates/tish/tests/error_source_location.rs +36 -0
  10. package/crates/tish/tests/fixtures/runtime_error_location.tish +5 -0
  11. package/crates/tish/tests/fixtures/trycatch_runtime_errors.tish +15 -0
  12. package/crates/tish/tests/fixtures/tty_capability.tish +9 -0
  13. package/crates/tish/tests/integration_test.rs +402 -91
  14. package/crates/tish/tests/trycatch_runtime_errors.rs +45 -0
  15. package/crates/tish/tests/tty_capability.rs +43 -0
  16. package/crates/tish_ast/src/ast.rs +37 -8
  17. package/crates/tish_builtins/Cargo.toml +2 -0
  18. package/crates/tish_builtins/src/array.rs +375 -13
  19. package/crates/tish_builtins/src/collections.rs +481 -0
  20. package/crates/tish_builtins/src/construct.rs +59 -19
  21. package/crates/tish_builtins/src/date.rs +538 -0
  22. package/crates/tish_builtins/src/globals.rs +86 -6
  23. package/crates/tish_builtins/src/iterator.rs +129 -0
  24. package/crates/tish_builtins/src/lib.rs +5 -0
  25. package/crates/tish_builtins/src/number.rs +96 -0
  26. package/crates/tish_builtins/src/object.rs +2 -2
  27. package/crates/tish_builtins/src/string.rs +19 -20
  28. package/crates/tish_builtins/src/symbol.rs +1 -1
  29. package/crates/tish_builtins/src/typedarrays.rs +298 -0
  30. package/crates/tish_bytecode/src/chunk.rs +69 -1
  31. package/crates/tish_bytecode/src/compiler.rs +933 -89
  32. package/crates/tish_bytecode/src/encoding.rs +2 -0
  33. package/crates/tish_bytecode/src/lib.rs +2 -1
  34. package/crates/tish_bytecode/src/opcode.rs +47 -4
  35. package/crates/tish_bytecode/src/serialize.rs +31 -1
  36. package/crates/tish_compile/Cargo.toml +1 -0
  37. package/crates/tish_compile/src/check.rs +774 -0
  38. package/crates/tish_compile/src/codegen.rs +2334 -349
  39. package/crates/tish_compile/src/infer.rs +1395 -6
  40. package/crates/tish_compile/src/lib.rs +50 -8
  41. package/crates/tish_compile/src/resolve.rs +584 -21
  42. package/crates/tish_compile/src/types.rs +106 -2
  43. package/crates/tish_compile_js/src/codegen.rs +67 -0
  44. package/crates/tish_compile_js/src/tests_jsx.rs +64 -0
  45. package/crates/tish_core/Cargo.toml +7 -1
  46. package/crates/tish_core/src/console_style.rs +11 -1
  47. package/crates/tish_core/src/json.rs +81 -38
  48. package/crates/tish_core/src/lib.rs +3 -0
  49. package/crates/tish_core/src/shape.rs +85 -0
  50. package/crates/tish_core/src/value.rs +679 -25
  51. package/crates/tish_core/src/vmref.rs +13 -8
  52. package/crates/tish_cranelift/src/link.rs +17 -4
  53. package/crates/tish_cranelift_runtime/Cargo.toml +1 -0
  54. package/crates/tish_eval/Cargo.toml +6 -0
  55. package/crates/tish_eval/src/eval.rs +665 -117
  56. package/crates/tish_eval/src/http.rs +4 -1
  57. package/crates/tish_eval/src/natives.rs +165 -13
  58. package/crates/tish_eval/src/value.rs +31 -13
  59. package/crates/tish_eval/src/value_convert.rs +10 -4
  60. package/crates/tish_ffi/Cargo.toml +26 -0
  61. package/crates/tish_ffi/src/lib.rs +518 -0
  62. package/crates/tish_ffi/tests/fixtures/testmod/Cargo.toml +18 -0
  63. package/crates/tish_ffi/tests/fixtures/testmod/src/lib.rs +46 -0
  64. package/crates/tish_ffi/tests/loader.rs +65 -0
  65. package/crates/tish_fmt/src/lib.rs +43 -5
  66. package/crates/tish_lexer/src/lib.rs +397 -9
  67. package/crates/tish_lexer/src/token.rs +7 -0
  68. package/crates/tish_lint/src/lib.rs +2 -10
  69. package/crates/tish_lsp/src/import_goto.rs +2 -0
  70. package/crates/tish_lsp/src/main.rs +439 -26
  71. package/crates/tish_native/src/build.rs +55 -1
  72. package/crates/tish_opt/src/lib.rs +126 -23
  73. package/crates/tish_parser/src/lib.rs +55 -1
  74. package/crates/tish_parser/src/parser.rs +456 -34
  75. package/crates/tish_pg/src/lib.rs +3 -3
  76. package/crates/tish_resolve/src/lib.rs +99 -59
  77. package/crates/tish_runtime/Cargo.toml +4 -0
  78. package/crates/tish_runtime/src/http.rs +66 -17
  79. package/crates/tish_runtime/src/http_fetch.rs +29 -8
  80. package/crates/tish_runtime/src/http_hyper.rs +25 -2
  81. package/crates/tish_runtime/src/lib.rs +299 -44
  82. package/crates/tish_runtime/src/promise.rs +328 -18
  83. package/crates/tish_runtime/src/timers.rs +13 -7
  84. package/crates/tish_runtime/src/tty.rs +226 -0
  85. package/crates/tish_runtime/src/ws.rs +35 -18
  86. package/crates/tish_runtime/tests/fetch_readable_stream.rs +2 -2
  87. package/crates/tish_ui/src/jsx.rs +10 -0
  88. package/crates/tish_ui/src/runtime/hooks.rs +19 -15
  89. package/crates/tish_ui/src/runtime/mod.rs +15 -12
  90. package/crates/tish_vm/Cargo.toml +14 -1
  91. package/crates/tish_vm/src/jit.rs +1050 -0
  92. package/crates/tish_vm/src/lib.rs +2 -0
  93. package/crates/tish_vm/src/vm.rs +1546 -202
  94. package/crates/tish_vm/tests/concurrent_shared_state.rs +140 -0
  95. package/crates/tish_wasm/src/lib.rs +6 -2
  96. package/crates/tish_wasm_runtime/src/gpu.rs +17 -1
  97. package/crates/tishlang_cargo_bindgen/src/classify.rs +1 -3
  98. package/crates/tishlang_cargo_bindgen/src/lib.rs +2 -2
  99. package/crates/tishlang_cargo_bindgen/src/metadata.rs +1 -1
  100. package/justfile +8 -0
  101. package/package.json +1 -1
  102. package/platform/darwin-arm64/tish +0 -0
  103. package/platform/darwin-x64/tish +0 -0
  104. package/platform/linux-arm64/tish +0 -0
  105. package/platform/linux-x64/tish +0 -0
  106. package/platform/win32-x64/tish.exe +0 -0
@@ -0,0 +1,140 @@
1
+ //! Regression: concurrent HTTP handlers that mutate shared module-level state must not deadlock.
2
+ //!
3
+ //! ## What this guards
4
+ //!
5
+ //! Under `send-values` (forced on by the `http` feature), `serve(port, handler)` runs the handler
6
+ //! closure — a `NativeFn` (`Arc<dyn Callable>`, `Send + Sync`) — **directly on each accept thread**
7
+ //! (`tish_runtime::http::worker_loop_direct`). So N concurrent requests execute the SAME handler in
8
+ //! parallel, all sharing the captured module scope through `Arc<Mutex>` (`VmRef`). A handler that
9
+ //! mutates a module-level `let` (a request counter / cache / rate-limiter) therefore has many threads
10
+ //! reading and writing the same scope cell at once.
11
+ //!
12
+ //! This test drives that exact path without a network: it pulls the handler `Value::Function` out of
13
+ //! a freshly-run program and invokes it from many OS threads while they all read-modify-write a shared
14
+ //! module-level `let`. It exists to catch a regression where the VM's variable-write path holds a
15
+ //! scope guard across a re-acquisition of the same lock (which would deadlock concurrent writers and
16
+ //! hang `serve`). The watchdog turns such a hang into a fast, explicit test failure instead of a
17
+ //! stuck CI job.
18
+ //!
19
+ //! Note on coverage: macOS `SO_REUSEPORT` funnels HTTP accepts to a single worker thread, so a
20
+ //! network-level test can't actually run two handlers concurrently on macOS. Calling the handler
21
+ //! closure directly does — and OS thread scheduling + mutex semantics are platform-independent, so
22
+ //! this reproduces the Linux multi-worker dispatch contention the bug was reported against.
23
+ #![cfg(feature = "send-values")]
24
+
25
+ use std::sync::atomic::{AtomicUsize, Ordering};
26
+ use std::sync::Arc;
27
+ use std::time::{Duration, Instant};
28
+ use tishlang_bytecode::compile;
29
+ use tishlang_core::{NativeFn, Value};
30
+ use tishlang_vm::Vm;
31
+
32
+ // `Value::Function` holds a `NativeFn` (= `Arc<dyn Callable>`, `Callable: Send + Sync` under
33
+ // send-values); invoke a handler via the trait-object method `.call(args)`.
34
+ type Handler = NativeFn;
35
+
36
+ /// Compile + run `src`, then pull a function it stored in a global back out.
37
+ fn export(vm: &Vm, name: &str) -> Handler {
38
+ match vm.get_global(name).unwrap_or_else(|| panic!("global `{name}` not found")) {
39
+ Value::Function(f) => f,
40
+ other => panic!("global `{name}` is not a function: {other:?}"),
41
+ }
42
+ }
43
+
44
+ fn read_num(obj: &Value, field: &str) -> f64 {
45
+ match obj {
46
+ Value::Object(o) => match o.borrow().strings.get(field) {
47
+ Some(Value::Number(n)) => *n,
48
+ other => panic!("stats.{field} is not a number: {other:?}"),
49
+ },
50
+ other => panic!("stats is not an object: {other:?}"),
51
+ }
52
+ }
53
+
54
+ #[test]
55
+ fn concurrent_handlers_mutating_shared_module_state_do_not_deadlock() {
56
+ // `handler`/`stats` are bare top-level assignments (undeclared names) -> stored in globals, so
57
+ // the test can pull them out. Both functions close over the same module-level `let`s, exactly as
58
+ // a real `serve` handler closes over module state. `served` is monotonic (only incremented), so
59
+ // it gives a deterministic plausibility bound even though the read-modify-write is racy.
60
+ let src = r#"
61
+ let active = 0
62
+ let maxActive = 0
63
+ let served = 0
64
+ fn handleRequest(req) {
65
+ active = active + 1
66
+ served = served + 1
67
+ if (active > maxActive) { maxActive = active }
68
+ let i = 0
69
+ while (i < 2000) { i = i + 1 } // brief CPU hold so handlers overlap
70
+ active = active - 1
71
+ return { status: 200, body: "ok" }
72
+ }
73
+ fn getStats() {
74
+ return { active: active, maxActive: maxActive, served: served }
75
+ }
76
+ handler = handleRequest
77
+ stats = getStats
78
+ "#;
79
+ let program = tishlang_parser::parse(src).expect("parse");
80
+ let chunk = compile(&program).expect("compile");
81
+ let mut vm = Vm::new();
82
+ vm.run(&chunk).expect("run top-level");
83
+ let handler = export(&vm, "handler");
84
+ let stats = export(&vm, "stats");
85
+
86
+ const THREADS: usize = 12;
87
+ const ITERS: usize = 100;
88
+ let total = THREADS * ITERS;
89
+
90
+ let done = Arc::new(AtomicUsize::new(0));
91
+ let start = Instant::now();
92
+ let mut handles = Vec::with_capacity(THREADS);
93
+ for t in 0..THREADS {
94
+ let h = handler.clone();
95
+ let done = Arc::clone(&done);
96
+ handles.push(std::thread::spawn(move || {
97
+ for i in 0..ITERS {
98
+ let resp = h.call(&[Value::Number((t * ITERS + i) as f64)]);
99
+ assert!(matches!(resp, Value::Object(_)), "handler must return a response object");
100
+ done.fetch_add(1, Ordering::Relaxed);
101
+ }
102
+ }));
103
+ }
104
+
105
+ // Watchdog: if concurrent writers deadlocked on the scope mutex, `done` stops advancing.
106
+ // Fail fast (and loudly) rather than hang the test runner.
107
+ let mut last = 0usize;
108
+ let mut last_change = Instant::now();
109
+ while done.load(Ordering::Relaxed) < total {
110
+ let cur = done.load(Ordering::Relaxed);
111
+ if cur != last {
112
+ last = cur;
113
+ last_change = Instant::now();
114
+ }
115
+ assert!(
116
+ last_change.elapsed() < Duration::from_secs(15),
117
+ "DEADLOCK regression: concurrent handlers stalled at {cur}/{total} (no progress for 15s)"
118
+ );
119
+ std::thread::sleep(Duration::from_millis(20));
120
+ }
121
+ for h in handles {
122
+ h.join().expect("a handler thread panicked");
123
+ }
124
+
125
+ // All calls returned without hanging. Read the shared counters back.
126
+ let s = stats.call(&[]);
127
+ let served = read_num(&s, "served");
128
+ let max_active = read_num(&s, "maxActive");
129
+ let active = read_num(&s, "active");
130
+ eprintln!(
131
+ "completed {total} concurrent calls / {THREADS} threads in {:?}; served={served}, maxActive={max_active}, active(final)={active}",
132
+ start.elapsed()
133
+ );
134
+
135
+ // `served` is monotonic, so it is deterministically in (0, total] regardless of lost updates.
136
+ assert!(served > 0.0 && served <= total as f64, "served={served} out of plausible range (0, {total}]");
137
+ // `maxActive` >= 2 proves at least two handlers were genuinely in-flight simultaneously, i.e. we
138
+ // actually exercised concurrent shared-state mutation (not an accidentally-serialized run).
139
+ assert!(max_active >= 2.0, "handlers never overlapped (maxActive={max_active}); test did not exercise concurrency");
140
+ }
@@ -379,10 +379,14 @@ fn main() {
379
379
  message: format!("Cannot write main.rs: {}", e),
380
380
  })?;
381
381
 
382
- // Build - use explicit target-dir so we know where the artifact is
382
+ // Build into a SHARED target dir (one per host), not per-program. The wasi runtime + embedded
383
+ // VM then compile ONCE and are reused by every wasi build; only each program's tiny main is
384
+ // rebuilt. Without this each program left its own multi-GB `target/` and a full-suite sweep
385
+ // would fill the disk (same issue fixed for cranelift; see full-backend-parity-plan.md A3).
386
+ // cargo's target lock serializes concurrent builds safely.
383
387
  let cargo = std::env::var("CARGO").unwrap_or_else(|_| "cargo".to_string());
384
388
  let bin_name = format!("tish_wasi_{}", stem);
385
- let target_dir = build_dir.join("target");
389
+ let target_dir = std::env::temp_dir().join("tishlang_wasi_target");
386
390
  let build_status = Command::new(&cargo)
387
391
  .current_dir(&build_dir)
388
392
  .env("CARGO_TARGET_DIR", &target_dir)
@@ -10,7 +10,7 @@
10
10
  //! command API is synchronous, so this covers it)
11
11
  //! - `js_new(ctorNameOrHandle, argsArray)`
12
12
  //! - `js_typeof(handle)` — debugging
13
- //! - `f32a(arr)` / `u16a(arr)` / `u8a(arr)` — tish `number[]` → real typed array
13
+ //! - `f32a(arr)` / `u16a(arr)` / `u8a(arr)` / `u32a(arr)` — tish `number[]` → real typed array
14
14
  //! - `request_animation_frame(cb)` — drive a render loop
15
15
  //!
16
16
  //! GPU/JS objects (device, queue, context, buffers, pipelines, textures,
@@ -284,6 +284,21 @@ fn ffi_u8a() -> Value {
284
284
  })
285
285
  }
286
286
 
287
+ fn ffi_u32a() -> Value {
288
+ Value::native(|args: &[Value]| {
289
+ let arr = match args.first() {
290
+ Some(Value::Array(a)) => a.clone(),
291
+ _ => return Value::Null,
292
+ };
293
+ let b = arr.borrow();
294
+ let ta = js_sys::Uint32Array::new_with_length(b.len() as u32);
295
+ for (i, v) in b.iter().enumerate() {
296
+ ta.set_index(i as u32, v.as_number().unwrap_or(0.0) as u32);
297
+ }
298
+ wrap(ta.into())
299
+ })
300
+ }
301
+
287
302
  // ---------------------------------------------------------------------------
288
303
  // requestAnimationFrame render loop
289
304
  // ---------------------------------------------------------------------------
@@ -362,6 +377,7 @@ fn install_ffi(vm: &mut Vm) {
362
377
  vm.set_global("f32a".into(), ffi_f32a());
363
378
  vm.set_global("u16a".into(), ffi_u16a());
364
379
  vm.set_global("u8a".into(), ffi_u8a());
380
+ vm.set_global("u32a".into(), ffi_u32a());
365
381
  vm.set_global("request_animation_frame".into(), ffi_request_animation_frame());
366
382
  }
367
383
 
@@ -39,9 +39,7 @@ fn classify_tish_abi(item: &ItemFn) -> Option<SignatureClass> {
39
39
  if value_args != 1 || sig.inputs.len() != 1 {
40
40
  return None;
41
41
  }
42
- let Some(ret_ty) = return_type_inner(&sig.output) else {
43
- return None;
44
- };
42
+ let ret_ty = return_type_inner(&sig.output)?;
45
43
  if !is_value_type(ret_ty) {
46
44
  return None;
47
45
  }
@@ -121,7 +121,7 @@ fn generate_from_resolved(
121
121
  impl BindgenConfig {
122
122
  /// Write using [`generate_from_registry_dependency`].
123
123
  pub fn write_files(&self) -> io::Result<()> {
124
- generate_from_registry_dependency(self).map_err(|e| io::Error::new(io::ErrorKind::Other, e))
124
+ generate_from_registry_dependency(self).map_err(io::Error::other)
125
125
  }
126
126
  }
127
127
 
@@ -199,7 +199,7 @@ fn render_generated_lib(
199
199
  name = rust_fn
200
200
  ),
201
201
  SignatureClass::SerializeRefToResultString => format!(
202
- "pub fn {name}(args: &[Value]) -> Value {{\n let Some(v) = args.first() else {{ return Value::Null }};\n match _tish_upstream::{name}(&tish_to_json(v)) {{\n Ok(s) => Value::String(Arc::from(s)),\n Err(_) => Value::Null,\n }}\n}}\n\n",
202
+ "pub fn {name}(args: &[Value]) -> Value {{\n let Some(v) = args.first() else {{ return Value::Null }};\n match _tish_upstream::{name}(&tish_to_json(v)) {{\n Ok(s) => Value::String(arcstr::ArcStr::from(s)),\n Err(_) => Value::Null,\n }}\n}}\n\n",
203
203
  name = rust_fn
204
204
  ),
205
205
  SignatureClass::DeserializeStrToResult => format!(
@@ -79,7 +79,7 @@ edition = "2021"
79
79
  && !pkg
80
80
  .targets
81
81
  .iter()
82
- .any(|t| t.kind.iter().any(|k| *k == TargetKind::Lib))
82
+ .any(|t| t.kind.contains(&TargetKind::Lib))
83
83
  {
84
84
  return Err(format!(
85
85
  "package `{}` has no src/ or lib target at {}",
package/justfile CHANGED
@@ -258,6 +258,14 @@ perf-suite *ARGS:
258
258
  perf-suite-gen:
259
259
  ./scripts/generate_perf_ci_main.sh
260
260
 
261
+ # HTTP throughput: tish vs Node, single vs multi-worker, plaintext + json (needs oha + jq)
262
+ perf-http *ARGS:
263
+ ./scripts/run_http_perf.sh {{ARGS}}
264
+
265
+ # Perf gauntlet: compute benchmarks vs Node, incl. known-fail targets to evolve past (needs node)
266
+ perf-gauntlet *ARGS:
267
+ ./scripts/run_perf_gauntlet.sh {{ARGS}}
268
+
261
269
  # Show binary sizes for different builds
262
270
  sizes:
263
271
  @echo "Building secure binary..."
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tishlang/tish",
3
- "version": "1.13.2",
3
+ "version": "2.0.0",
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