@tishlang/tish-format 3.15.1 → 3.15.3

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-format CHANGED
Binary file
@@ -363,6 +363,18 @@ fn parse_key<'a>(input: &'a str, cache: &mut KeyCache) -> Result<(Arc<str>, &'a
363
363
  }
364
364
  }
365
365
 
366
+ /// The four hex digits after `\\u`, consuming them (and counting their bytes); None if they
367
+ /// aren't four hex digits.
368
+ fn hex4(chars: &mut core::iter::Peekable<core::str::Chars<'_>>, byte_count: &mut usize) -> Option<u32> {
369
+ let mut hex = String::new();
370
+ for _ in 0..4 {
371
+ let c = chars.next()?;
372
+ hex.push(c);
373
+ *byte_count += c.len_utf8();
374
+ }
375
+ u32::from_str_radix(&hex, 16).ok()
376
+ }
377
+
366
378
  /// Decode a JSON string that contains at least one escape (the slow path).
367
379
  fn parse_string_escaped(input: &str) -> Result<(Value, &str), String> {
368
380
  let input = &input[1..]; // skip opening quote
@@ -404,19 +416,39 @@ fn parse_string_escaped(input: &str) -> Result<(Value, &str), String> {
404
416
  result.push('/');
405
417
  byte_count += 1;
406
418
  }
419
+ Some('b') => {
420
+ result.push('\u{8}');
421
+ byte_count += 1;
422
+ }
423
+ Some('f') => {
424
+ result.push('\u{c}');
425
+ byte_count += 1;
426
+ }
407
427
  Some('u') => {
408
428
  byte_count += 1;
409
- let mut hex = String::new();
410
- for _ in 0..4 {
411
- if let Some(c) = chars.next() {
412
- hex.push(c);
413
- byte_count += c.len_utf8();
414
- }
415
- }
416
- if let Ok(n) = u32::from_str_radix(&hex, 16) {
417
- if let Some(c) = char::from_u32(n) {
418
- result.push(c);
429
+ match hex4(&mut chars, &mut byte_count) {
430
+ // A UTF-16 surrogate pair (`\ud83d\ude00`) is one character. A lone
431
+ // surrogate has no character of its own, so it reads as U+FFFD.
432
+ Some(high @ 0xD800..=0xDBFF) => {
433
+ let mut ahead = chars.clone();
434
+ let mut ahead_bytes = 0;
435
+ let low = if ahead.next() == Some('\\') && ahead.next() == Some('u') {
436
+ hex4(&mut ahead, &mut ahead_bytes)
437
+ } else {
438
+ None
439
+ };
440
+ match low {
441
+ Some(low @ 0xDC00..=0xDFFF) => {
442
+ chars = ahead;
443
+ byte_count += 2 + ahead_bytes;
444
+ let n = 0x10000 + ((high - 0xD800) << 10) + (low - 0xDC00);
445
+ result.push(char::from_u32(n).unwrap_or('\u{FFFD}'));
446
+ }
447
+ _ => result.push('\u{FFFD}'),
448
+ }
419
449
  }
450
+ Some(n) => result.push(char::from_u32(n).unwrap_or('\u{FFFD}')),
451
+ None => {}
420
452
  }
421
453
  }
422
454
  Some(c) => {
@@ -664,3 +696,31 @@ mod tests {
664
696
  }
665
697
  }
666
698
  }
699
+
700
+ #[cfg(test)]
701
+ mod escape_tests {
702
+ use super::json_parse;
703
+ use crate::Value;
704
+
705
+ fn text(json: &str) -> String {
706
+ match json_parse(json).unwrap() {
707
+ Value::String(s) => s.to_string(),
708
+ other => panic!("not a string: {other:?}"),
709
+ }
710
+ }
711
+
712
+ #[test]
713
+ fn surrogate_pairs_are_one_character() {
714
+ assert_eq!(text(r#""😀 ok""#), "😀 ok");
715
+ assert_eq!(text(r#""a😀b""#), "a😀b");
716
+ assert_eq!(text(r#""é""#), "é");
717
+ }
718
+
719
+ #[test]
720
+ fn lone_surrogates_and_short_escapes() {
721
+ assert_eq!(text(r#""\ud83d x""#), "\u{FFFD} x");
722
+ assert_eq!(text(r#""\ude00""#), "\u{FFFD}");
723
+ assert_eq!(text(r#""\ud83dA""#), "\u{FFFD}A");
724
+ assert_eq!(text(r#""\b\f\n""#), "\u{8}\u{c}\n");
725
+ }
726
+ }
@@ -15,7 +15,7 @@
15
15
  //! | feature `send-values` | `VmRef<T>` | `NativeFn` | targets |
16
16
  //! |---------------------------|-------------------------|----------------------------------|--------------------------------------------------|
17
17
  //! | **off** *(default)* | `Rc<RefCell<T>>` | `Rc<dyn Fn + 'static>` | wasm32, wasi, interpreter, cranelift/llvm VMs |
18
- //! | **on** | `Arc<Mutex<T>>` | `Arc<dyn Fn + Send + Sync>` | Rust native with `http` enabled (server workloads) |
18
+ //! | **on** | `Arc<RwLock<T>>` | `Arc<dyn Fn + Send + Sync>` | Rust native with `http` enabled (server workloads) |
19
19
  //!
20
20
  //! The *API* is identical in both configurations (`borrow` / `borrow_mut`
21
21
  //! / `ptr_eq` / `Clone`), so every existing call site in the workspace
@@ -28,7 +28,7 @@
28
28
  //! bit-identical to the pre-migration baseline.
29
29
  //! * **Rust native, non-server**: same — `send-values` only activates
30
30
  //! when something in the dependency graph (usually `http`) needs it.
31
- //! * **Rust native with server**: `Arc<Mutex<T>>` pays ~3–5 ns per
31
+ //! * **Rust native with server**: `Arc<RwLock<T>>` pays ~3–5 ns per
32
32
  //! `borrow` in the uncontended case (single atomic CAS). On Tish's
33
33
  //! hot paths — roughly 6–12 borrows per request — that's ~30–60 ns of
34
34
  //! overhead. In exchange we get `N×` handler scaling across cores,
@@ -45,7 +45,7 @@
45
45
  //! ```
46
46
  //!
47
47
  //! Returned guard types (`VmReadGuard<'_, T>`, `VmWriteGuard<'_, T>`) are
48
- //! type aliases that pick `Ref`/`RefMut` or `MutexGuard` depending on the
48
+ //! type aliases that pick `Ref`/`RefMut` or `RwLock` read/write guards depending on the
49
49
  //! feature. They both `Deref` (and, for write guards, `DerefMut`) to `T`
50
50
  //! just like the underlying types.
51
51
 
@@ -113,45 +113,45 @@ mod imp {
113
113
  }
114
114
 
115
115
  // --------------------------------------------------------------------------
116
- // Thread-safe backing store (opt-in): Arc<Mutex<T>>
116
+ // Thread-safe backing store (opt-in): Arc<RwLock<T>>
117
117
  // --------------------------------------------------------------------------
118
118
  #[cfg(feature = "send-values")]
119
119
  mod imp {
120
- use parking_lot::Mutex;
120
+ use parking_lot::RwLock;
121
121
  use std::sync::Arc;
122
122
 
123
123
  #[derive(Default)]
124
- pub struct VmRef<T: ?Sized>(pub(super) Arc<Mutex<T>>);
124
+ pub struct VmRef<T: ?Sized>(pub(super) Arc<RwLock<T>>);
125
125
 
126
- /// Read guard alias. On the multi-threaded path both readers and
127
- /// writers share a single `MutexGuard` (exclusive access).
128
- pub type ReadGuard<'a, T> = parking_lot::MutexGuard<'a, T>;
129
- /// Write guard alias.
130
- pub type WriteGuard<'a, T> = parking_lot::MutexGuard<'a, T>;
126
+ /// Read guard alias. Readers share the lock, like `RefCell::borrow`: native codegen reads the
127
+ /// same cell more than once within one statement (`{ a: x, b: n - x }` → two
128
+ /// `(*x.borrow())` temporaries alive until the statement ends), which an exclusive lock turned
129
+ /// into a self-deadlock.
130
+ pub type ReadGuard<'a, T> = parking_lot::RwLockReadGuard<'a, T>;
131
+ /// Write guard alias (exclusive).
132
+ pub type WriteGuard<'a, T> = parking_lot::RwLockWriteGuard<'a, T>;
131
133
 
132
134
  impl<T> VmRef<T> {
133
135
  #[inline]
134
136
  pub fn new(value: T) -> Self {
135
- VmRef(Arc::new(Mutex::new(value)))
137
+ VmRef(Arc::new(RwLock::new(value)))
136
138
  }
137
139
  }
138
140
 
139
141
  impl<T: ?Sized> VmRef<T> {
140
- /// Acquire the inner mutex. `parking_lot::Mutex` is used (not
141
- /// `std::sync::Mutex`): its uncontended lock is a single atomic with
142
- /// no pthread syscall — a profile of object/array-heavy code showed
143
- /// `pthread_mutex_lock/unlock` as a top cost under send-values, since
144
- /// every property/element access locks. It also has no poisoning, so
145
- /// there is no `Result` to swallow (a handler panic aborts the thread
146
- /// regardless).
142
+ /// Shared access. `read_recursive` so a thread that already holds a read guard can take
143
+ /// another even while a writer on another thread waits (a plain `read` would queue behind
144
+ /// that writer and deadlock). `parking_lot` rather than `std`: its uncontended lock is a
145
+ /// single atomic, and every property/element access takes one.
147
146
  #[inline]
148
147
  pub fn borrow(&self) -> ReadGuard<'_, T> {
149
- self.0.lock()
148
+ self.0.read_recursive()
150
149
  }
151
150
 
151
+ /// Exclusive access.
152
152
  #[inline]
153
153
  pub fn borrow_mut(&self) -> WriteGuard<'_, T> {
154
- self.0.lock()
154
+ self.0.write()
155
155
  }
156
156
 
157
157
  #[inline]
@@ -207,3 +207,38 @@ impl<T: fmt::Debug> fmt::Debug for VmRef<T> {
207
207
  write!(f, "RefCell {{ value: {:?} }}", &*self.borrow())
208
208
  }
209
209
  }
210
+
211
+ #[cfg(all(test, feature = "send-values"))]
212
+ mod send_values_tests {
213
+ use super::VmRef;
214
+
215
+ /// Native codegen keeps `(*x.borrow())` temporaries alive to the end of a statement, so one
216
+ /// statement can read the same cell twice: `{ a: x, b: n - x }`. That must not deadlock.
217
+ #[test]
218
+ fn two_reads_of_one_cell_at_once() {
219
+ let x = VmRef::new(2.0_f64);
220
+ let sum = *x.borrow() + *x.borrow();
221
+ assert_eq!(sum, 4.0);
222
+ *x.borrow_mut() = sum;
223
+ assert_eq!(*x.borrow(), 4.0);
224
+ }
225
+
226
+ /// Readers on other threads still see writes, and a held read guard doesn't block another
227
+ /// reader while a writer waits elsewhere.
228
+ #[test]
229
+ fn readers_and_writers_across_threads() {
230
+ let x = VmRef::new(0_u32);
231
+ let held = x.borrow();
232
+ let writer = {
233
+ let x = x.clone();
234
+ std::thread::spawn(move || {
235
+ *x.borrow_mut() += 1;
236
+ })
237
+ };
238
+ std::thread::sleep(std::time::Duration::from_millis(20));
239
+ assert_eq!(*x.borrow(), 0, "a second read while a writer waits");
240
+ drop(held);
241
+ writer.join().unwrap();
242
+ assert_eq!(*x.borrow(), 1);
243
+ }
244
+ }
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "tishlang_fmt"
3
- version = "3.15.1"
3
+ version = "3.15.3"
4
4
  edition = "2021"
5
5
  description = "Opinionated formatter for Tish source (parse → pretty-print)"
6
6
  license-file = { workspace = true }
@@ -236,14 +236,7 @@ pub fn build_via_cargo_with_config(
236
236
  .as_deref()
237
237
  .map_or(true, |t| cfg!(target_os = "macos") && t.ends_with("-apple-darwin"));
238
238
  if build_config.artifact == NativeArtifact::FfiModule {
239
- let guest_path = Path::new(&runtime_path)
240
- .parent()
241
- .map(|crates| crates.join("tish_ffi_guest"))
242
- .ok_or_else(|| format!("Cannot locate tish_ffi_guest next to {}", runtime_path))?;
243
- more_deps.push_str(&format!(
244
- "\ntishlang_ffi_guest = {{ path = {:?} }}\n",
245
- guest_path.display().to_string().replace('\\', "/")
246
- ));
239
+ more_deps.push_str(&ffi_guest_dep_toml(&runtime_path)?);
247
240
  }
248
241
  if use_fast_alloc {
249
242
  more_deps.push_str("\nmimalloc = \"0.1\"\n");
@@ -518,6 +511,24 @@ fn runtime_dep_toml(runtime_path: &str, features_str: &str) -> String {
518
511
  }
519
512
  }
520
513
 
514
+ /// The `tishlang_ffi_guest` dependency line for an FFI module (same source policy as the runtime).
515
+ /// A released compiler's module must take it from the registry: as a path it would bring its own
516
+ /// path-built `tishlang_core` next to the registry one the runtime uses, and the two `Value` types
517
+ /// don't match.
518
+ fn ffi_guest_dep_toml(runtime_path: &str) -> Result<String, String> {
519
+ if let Some(v) = released_compiler_version() {
520
+ return Ok(format!("\ntishlang_ffi_guest = {{ version = {:?} }}\n", v));
521
+ }
522
+ let guest_path = Path::new(runtime_path)
523
+ .parent()
524
+ .map(|crates| crates.join("tish_ffi_guest"))
525
+ .ok_or_else(|| format!("Cannot locate tish_ffi_guest next to {}", runtime_path))?;
526
+ Ok(format!(
527
+ "\ntishlang_ffi_guest = {{ path = {:?} }}\n",
528
+ guest_path.display().to_string().replace('\\', "/")
529
+ ))
530
+ }
531
+
521
532
  /// The optional `tishlang_ui` dependency line (same source policy as the runtime).
522
533
  fn ui_dep_toml(runtime_path: &str, rust_code: &str) -> String {
523
534
  if !rust_code.contains("tishlang_ui") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tishlang/tish-format",
3
- "version": "3.15.1",
3
+ "version": "3.15.3",
4
4
  "description": "Tish code formatter",
5
5
  "license": "PIF",
6
6
  "repository": {
Binary file
Binary file
Binary file
Binary file
Binary file