@tishlang/tish-format 2.43.8 → 3.1.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 (47) hide show
  1. package/bin/tish-format +0 -0
  2. package/crates/tish/tests/fixtures/cargo_example_project/crates/demo-shim/src/lib.rs +21 -0
  3. package/crates/tish/tests/fixtures/cargo_example_project/crates/demo-shim/tish.d.tish +4 -0
  4. package/crates/tish/tests/fixtures/cargo_example_project/src/main.tish +5 -1
  5. package/crates/tish/tests/regr_typed_externs.rs +59 -0
  6. package/crates/tish/tests/scope_merge.rs +31 -0
  7. package/crates/tish_build_utils/src/lib.rs +131 -6
  8. package/crates/tish_builtins/src/globals.rs +27 -0
  9. package/crates/tish_compile/src/codegen.rs +1498 -132
  10. package/crates/tish_compile/src/resolve.rs +65 -9
  11. package/crates/tish_compile/src/schemes.rs +60 -5
  12. package/crates/tish_compile/src/types.rs +64 -32
  13. package/crates/tish_compile/tests/regr_556_param_name_collision.rs +40 -0
  14. package/crates/tish_compile/tests/regr_558_struct_array_write.rs +65 -0
  15. package/crates/tish_compile/tests/regr_558b_struct_field_read.rs +21 -0
  16. package/crates/tish_compile/tests/regr_558c_wrapped_struct_array.rs +64 -0
  17. package/crates/tish_compile/tests/regr_558d_native_vec_mutators.rs +156 -0
  18. package/crates/tish_compile/tests/regr_562_wrapped_struct_array_push.rs +50 -0
  19. package/crates/tish_compile/tests/regr_fixed_native_coercions.rs +69 -0
  20. package/crates/tish_compile/tests/regr_gba_struct_reflection.rs +106 -0
  21. package/crates/tish_compile/tests/regression/name_collision_lib.tish +6 -0
  22. package/crates/tish_compile/tests/regression/name_collision_main.tish +13 -0
  23. package/crates/tish_compile/tests/regression/struct_array_write.tish +22 -0
  24. package/crates/tish_compile/tests/regression/struct_field_return_read.tish +10 -0
  25. package/crates/tish_core/src/console_style.rs +4 -0
  26. package/crates/tish_core/src/json.rs +8 -0
  27. package/crates/tish_core/src/shape.rs +28 -3
  28. package/crates/tish_core/src/value.rs +123 -0
  29. package/crates/tish_fmt/Cargo.toml +1 -1
  30. package/crates/tish_native/src/build.rs +4 -0
  31. package/crates/tish_runtime/Cargo.toml +20 -1
  32. package/crates/tish_runtime/src/crypto.rs +127 -0
  33. package/crates/tish_runtime/src/encoding.rs +157 -0
  34. package/crates/tish_runtime/src/fs_ext.rs +27 -0
  35. package/crates/tish_runtime/src/lib.rs +33 -0
  36. package/crates/tish_runtime/src/net.rs +482 -0
  37. package/crates/tish_runtime/src/process_spawn.rs +508 -0
  38. package/crates/tish_runtime/src/ws.rs +206 -23
  39. package/crates/tish_runtime/src/zip.rs +196 -0
  40. package/crates/tish_runtime_gba/src/lib.rs +188 -4
  41. package/crates/tish_vm/src/vm.rs +48 -0
  42. package/package.json +1 -1
  43. package/platform/darwin-arm64/tish-fmt +0 -0
  44. package/platform/darwin-x64/tish-fmt +0 -0
  45. package/platform/linux-arm64/tish-fmt +0 -0
  46. package/platform/linux-x64/tish-fmt +0 -0
  47. package/platform/win32-x64/tish-fmt.exe +0 -0
@@ -0,0 +1,508 @@
1
+ //! Pipe-based child-process streaming for Tish (`tish:process` spawn/read/write/kill/wait),
2
+ //! behind the `process` feature.
3
+ //!
4
+ //! Unlike `tish:pty` (a real pseudoterminal — line editing, SIGWINCH, isatty) and unlike
5
+ //! `process.execCapture` (run-to-completion, fully buffered), this spawns a child with PLAIN
6
+ //! PIPES for stdin/stdout/stderr and streams them incrementally. That is the shape language
7
+ //! servers (LSP), debug adapters (DAP), MCP servers, and the node extension host need: their
8
+ //! stdio carries framed protocols (`Content-Length` headers, newline-delimited JSON) that a
9
+ //! pseudoterminal's line discipline would corrupt, and they are long-lived, so capture-only
10
+ //! `execCapture` cannot drive them.
11
+ //!
12
+ //! import { spawn, readStdout, readStderr, writeStdin, closeStdin, wait, kill, pid } from 'tish:process'
13
+ //!
14
+ //! - `spawn({ program, args?, cwd?, env? }) -> id | null`
15
+ //! - `readStdout(id, timeoutMs?) -> string | null` (`""` = live but no output yet; `null` = EOF/unknown)
16
+ //! - `readStderr(id, timeoutMs?) -> string | null`
17
+ //! - `writeStdin(id, data) -> bool`
18
+ //! - `closeStdin(id) -> bool` (drop the child's stdin → sends EOF; some servers need it)
19
+ //! - `wait(id, timeoutMs?) -> number | null` (exit code once exited; `null` while still running / unknown)
20
+ //! - `kill(id) -> bool`
21
+ //! - `pid(id) -> number | null`
22
+ //!
23
+ //! Two per-session reader threads (stdout, stderr) fill byte buffers that the read fns drain at
24
+ //! a UTF-8 boundary — an incomplete trailing multibyte sequence is held for the next read (no
25
+ //! mojibake). A global `OnceLock<Mutex<HashMap<id, ProcSession>>>` registry mirrors `pty.rs`;
26
+ //! errors surface as `null` / `false` rather than panicking.
27
+
28
+ use std::collections::HashMap;
29
+ use std::io::{Read, Write};
30
+ use std::process::{Child, ChildStdin, Command, Stdio};
31
+ use std::sync::atomic::{AtomicU64, Ordering};
32
+ use std::sync::{Arc, Condvar, Mutex, OnceLock};
33
+ use std::time::{Duration, Instant};
34
+
35
+ use tishlang_core::Value;
36
+
37
+ /// Output accumulated by a reader thread, drained by `readStdout` / `readStderr`.
38
+ struct StreamBuf {
39
+ data: Vec<u8>,
40
+ eof: bool,
41
+ }
42
+
43
+ type SharedBuf = Arc<(Mutex<StreamBuf>, Condvar)>;
44
+
45
+ struct ProcSession {
46
+ child: Mutex<Child>,
47
+ stdin: Mutex<Option<ChildStdin>>,
48
+ out: SharedBuf,
49
+ err: SharedBuf,
50
+ pid: Option<u32>,
51
+ }
52
+
53
+ fn sessions() -> &'static Mutex<HashMap<u64, ProcSession>> {
54
+ static S: OnceLock<Mutex<HashMap<u64, ProcSession>>> = OnceLock::new();
55
+ S.get_or_init(|| Mutex::new(HashMap::new()))
56
+ }
57
+
58
+ static NEXT_ID: AtomicU64 = AtomicU64::new(1);
59
+
60
+ // --- Value helpers (mirror pty.rs idioms) ---
61
+
62
+ fn arg_u64(args: &[Value], i: usize) -> Option<u64> {
63
+ match args.get(i) {
64
+ Some(Value::Number(n)) if n.is_finite() && *n >= 0.0 => Some(*n as u64),
65
+ _ => None,
66
+ }
67
+ }
68
+
69
+ fn arg_timeout(args: &[Value], i: usize) -> u64 {
70
+ match args.get(i) {
71
+ Some(Value::Number(n)) if n.is_finite() && *n >= 0.0 => *n as u64,
72
+ _ => 0,
73
+ }
74
+ }
75
+
76
+ fn obj_field(o: &Value, key: &str) -> Option<Value> {
77
+ if let Value::Object(m) = o {
78
+ return m.borrow().strings.get(key).cloned();
79
+ }
80
+ None
81
+ }
82
+
83
+ fn obj_str(o: &Value, key: &str) -> Option<String> {
84
+ match obj_field(o, key) {
85
+ Some(Value::String(s)) => Some(s.to_string()),
86
+ Some(Value::Null) | None => None,
87
+ Some(v) => Some(v.to_display_string()),
88
+ }
89
+ }
90
+
91
+ fn new_buf() -> SharedBuf {
92
+ Arc::new((
93
+ Mutex::new(StreamBuf {
94
+ data: Vec::new(),
95
+ eof: false,
96
+ }),
97
+ Condvar::new(),
98
+ ))
99
+ }
100
+
101
+ /// Reader thread: block on a pipe, append bytes, wake any waiting reader; mark EOF on 0/err.
102
+ fn spawn_reader<R: Read + Send + 'static>(mut r: R, buf: SharedBuf) {
103
+ std::thread::spawn(move || {
104
+ let mut tmp = [0u8; 8192];
105
+ loop {
106
+ match r.read(&mut tmp) {
107
+ Ok(0) => {
108
+ let (lock, cv) = &*buf;
109
+ if let Ok(mut b) = lock.lock() {
110
+ b.eof = true;
111
+ }
112
+ cv.notify_all();
113
+ break;
114
+ }
115
+ Ok(n) => {
116
+ let (lock, cv) = &*buf;
117
+ if let Ok(mut b) = lock.lock() {
118
+ b.data.extend_from_slice(&tmp[..n]);
119
+ }
120
+ cv.notify_all();
121
+ }
122
+ Err(_) => {
123
+ let (lock, cv) = &*buf;
124
+ if let Ok(mut b) = lock.lock() {
125
+ b.eof = true;
126
+ }
127
+ cv.notify_all();
128
+ break;
129
+ }
130
+ }
131
+ }
132
+ });
133
+ }
134
+
135
+ /// Drain a stream buffer as a string (possibly `""` within the timeout), or `null` at EOF.
136
+ /// Drains only a valid UTF-8 prefix, holding any incomplete trailing multibyte for next time
137
+ /// (identical semantics to `pty::pty_read`).
138
+ fn drain_stream(buf: &SharedBuf, timeout_ms: u64) -> Value {
139
+ let (lock, cv) = &**buf;
140
+ let mut b = match lock.lock() {
141
+ Ok(b) => b,
142
+ Err(_) => return Value::Null,
143
+ };
144
+ if b.data.is_empty() && !b.eof && timeout_ms > 0 {
145
+ let res = cv.wait_timeout_while(b, Duration::from_millis(timeout_ms), |b| {
146
+ b.data.is_empty() && !b.eof
147
+ });
148
+ b = match res {
149
+ Ok((g, _)) => g,
150
+ Err(e) => e.into_inner().0,
151
+ };
152
+ }
153
+ if b.data.is_empty() {
154
+ if b.eof {
155
+ return Value::Null;
156
+ }
157
+ return Value::String("".into());
158
+ }
159
+ let valid = match std::str::from_utf8(&b.data) {
160
+ Ok(_) => b.data.len(),
161
+ Err(e) => e.valid_up_to(),
162
+ };
163
+ if valid == 0 {
164
+ if b.eof {
165
+ let s = String::from_utf8_lossy(&b.data).into_owned();
166
+ b.data.clear();
167
+ return Value::String(s.into());
168
+ }
169
+ return Value::String("".into());
170
+ }
171
+ let out: Vec<u8> = b.data.drain(..valid).collect();
172
+ let s = String::from_utf8(out).unwrap_or_default();
173
+ Value::String(s.into())
174
+ }
175
+
176
+ /// `spawn({ program, args?, cwd?, env? })` → a stable id for read/write/wait/kill, or `null`
177
+ /// on failure. `program` is required; `args` is an array of strings; `env` entries are ADDED to
178
+ /// the inherited environment.
179
+ pub fn process_spawn(args: &[Value]) -> Value {
180
+ let null = Value::Null;
181
+ let opts = args.first().unwrap_or(&null);
182
+
183
+ let program = match obj_str(opts, "program") {
184
+ Some(p) if !p.is_empty() => p,
185
+ _ => return Value::Null,
186
+ };
187
+
188
+ let mut cmd = Command::new(program);
189
+ if let Some(Value::Array(a)) = obj_field(opts, "args") {
190
+ for v in a.borrow().iter() {
191
+ cmd.arg(v.to_display_string());
192
+ }
193
+ }
194
+ if let Some(cwd) = obj_str(opts, "cwd") {
195
+ cmd.current_dir(cwd);
196
+ }
197
+ if let Some(Value::Object(em)) = obj_field(opts, "env") {
198
+ for (k, v) in em.borrow().strings.iter() {
199
+ cmd.env(k.as_ref(), v.to_display_string());
200
+ }
201
+ }
202
+
203
+ // `detached: true` — fire-and-forget launch (a GUI app, an ssh -L tunnel, the `dune` CLI's own
204
+ // window op): null stdio + a fresh process group so the child outlives this process and isn't hit
205
+ // by signals to the parent's group. Returns the child PID (a number) with NO session registered —
206
+ // there's nothing to read/write/wait/kill.
207
+ if matches!(obj_field(opts, "detached"), Some(Value::Bool(true))) {
208
+ cmd.stdin(Stdio::null())
209
+ .stdout(Stdio::null())
210
+ .stderr(Stdio::null());
211
+ #[cfg(unix)]
212
+ {
213
+ use std::os::unix::process::CommandExt;
214
+ cmd.process_group(0);
215
+ }
216
+ return match cmd.spawn() {
217
+ Ok(child) => Value::Number(child.id() as f64),
218
+ Err(_) => Value::Null,
219
+ };
220
+ }
221
+
222
+ cmd.stdin(Stdio::piped())
223
+ .stdout(Stdio::piped())
224
+ .stderr(Stdio::piped());
225
+
226
+ let mut child = match cmd.spawn() {
227
+ Ok(c) => c,
228
+ Err(_) => return Value::Null,
229
+ };
230
+
231
+ let stdin = child.stdin.take();
232
+ let stdout = match child.stdout.take() {
233
+ Some(s) => s,
234
+ None => return Value::Null,
235
+ };
236
+ let stderr = match child.stderr.take() {
237
+ Some(s) => s,
238
+ None => return Value::Null,
239
+ };
240
+ let pid = Some(child.id());
241
+
242
+ let out = new_buf();
243
+ let err = new_buf();
244
+ spawn_reader(stdout, out.clone());
245
+ spawn_reader(stderr, err.clone());
246
+
247
+ let id = NEXT_ID.fetch_add(1, Ordering::SeqCst);
248
+ let session = ProcSession {
249
+ child: Mutex::new(child),
250
+ stdin: Mutex::new(stdin),
251
+ out,
252
+ err,
253
+ pid,
254
+ };
255
+ match sessions().lock() {
256
+ Ok(mut g) => {
257
+ g.insert(id, session);
258
+ }
259
+ Err(_) => return Value::Null,
260
+ }
261
+ Value::Number(id as f64)
262
+ }
263
+
264
+ fn read_from(args: &[Value], stderr: bool) -> Value {
265
+ let id = match arg_u64(args, 0) {
266
+ Some(x) => x,
267
+ None => return Value::Null,
268
+ };
269
+ let timeout_ms = arg_timeout(args, 1);
270
+ let buf = {
271
+ let g = match sessions().lock() {
272
+ Ok(g) => g,
273
+ Err(_) => return Value::Null,
274
+ };
275
+ match g.get(&id) {
276
+ Some(s) => {
277
+ if stderr {
278
+ s.err.clone()
279
+ } else {
280
+ s.out.clone()
281
+ }
282
+ }
283
+ None => return Value::Null,
284
+ }
285
+ };
286
+ drain_stream(&buf, timeout_ms)
287
+ }
288
+
289
+ /// `readStdout(id, timeoutMs?)` → available stdout, `""` (live, none yet), or `null` at EOF/unknown.
290
+ pub fn process_read_stdout(args: &[Value]) -> Value {
291
+ read_from(args, false)
292
+ }
293
+
294
+ /// `readStderr(id, timeoutMs?)` → available stderr, `""` (live, none yet), or `null` at EOF/unknown.
295
+ pub fn process_read_stderr(args: &[Value]) -> Value {
296
+ read_from(args, true)
297
+ }
298
+
299
+ /// `writeStdin(id, data)` → feed bytes to the child's stdin. Returns whether the write succeeded.
300
+ pub fn process_write_stdin(args: &[Value]) -> Value {
301
+ let id = match arg_u64(args, 0) {
302
+ Some(x) => x,
303
+ None => return Value::Bool(false),
304
+ };
305
+ let data = match args.get(1) {
306
+ Some(Value::String(s)) => s.to_string(),
307
+ Some(v) => v.to_display_string(),
308
+ None => return Value::Bool(false),
309
+ };
310
+ let g = match sessions().lock() {
311
+ Ok(g) => g,
312
+ Err(_) => return Value::Bool(false),
313
+ };
314
+ let s = match g.get(&id) {
315
+ Some(s) => s,
316
+ None => return Value::Bool(false),
317
+ };
318
+ let mut w = match s.stdin.lock() {
319
+ Ok(w) => w,
320
+ Err(_) => return Value::Bool(false),
321
+ };
322
+ match w.as_mut() {
323
+ Some(stdin) => match stdin.write_all(data.as_bytes()).and_then(|_| stdin.flush()) {
324
+ Ok(_) => Value::Bool(true),
325
+ Err(_) => Value::Bool(false),
326
+ },
327
+ None => Value::Bool(false),
328
+ }
329
+ }
330
+
331
+ /// `closeStdin(id)` → drop the child's stdin, sending EOF. Returns whether stdin was open.
332
+ pub fn process_close_stdin(args: &[Value]) -> Value {
333
+ let id = match arg_u64(args, 0) {
334
+ Some(x) => x,
335
+ None => return Value::Bool(false),
336
+ };
337
+ let g = match sessions().lock() {
338
+ Ok(g) => g,
339
+ Err(_) => return Value::Bool(false),
340
+ };
341
+ let s = match g.get(&id) {
342
+ Some(s) => s,
343
+ None => return Value::Bool(false),
344
+ };
345
+ let mut w = match s.stdin.lock() {
346
+ Ok(w) => w,
347
+ Err(_) => return Value::Bool(false),
348
+ };
349
+ // Dropping the ChildStdin closes the pipe.
350
+ Value::Bool(w.take().is_some())
351
+ }
352
+
353
+ /// `wait(id, timeoutMs?)` → the child's exit code once it has exited, or `null` while it is still
354
+ /// running (or for an unknown id). Polls up to `timeoutMs` (0 = a single non-blocking check).
355
+ pub fn process_wait(args: &[Value]) -> Value {
356
+ let id = match arg_u64(args, 0) {
357
+ Some(x) => x,
358
+ None => return Value::Null,
359
+ };
360
+ let timeout_ms = arg_timeout(args, 1);
361
+ let deadline = Instant::now() + Duration::from_millis(timeout_ms);
362
+ loop {
363
+ // Compute try_wait() into an owned result inside a tight scope so the sessions guard and
364
+ // the child guard both drop before the loop's sleep (avoids holding locks across the wait).
365
+ let status = {
366
+ let g = match sessions().lock() {
367
+ Ok(g) => g,
368
+ Err(_) => return Value::Null,
369
+ };
370
+ let s = match g.get(&id) {
371
+ Some(s) => s,
372
+ None => return Value::Null,
373
+ };
374
+ let mut c = match s.child.lock() {
375
+ Ok(c) => c,
376
+ Err(_) => return Value::Null,
377
+ };
378
+ c.try_wait()
379
+ };
380
+ match status {
381
+ Ok(Some(st)) => return Value::Number(st.code().unwrap_or(-1) as f64),
382
+ Ok(None) => {}
383
+ Err(_) => return Value::Null,
384
+ }
385
+ if Instant::now() >= deadline {
386
+ return Value::Null;
387
+ }
388
+ std::thread::sleep(Duration::from_millis(10));
389
+ }
390
+ }
391
+
392
+ /// `kill(id)` → terminate the child and drop the session. Returns whether the id was live.
393
+ pub fn process_kill(args: &[Value]) -> Value {
394
+ let id = match arg_u64(args, 0) {
395
+ Some(x) => x,
396
+ None => return Value::Bool(false),
397
+ };
398
+ let sess = {
399
+ let mut g = match sessions().lock() {
400
+ Ok(g) => g,
401
+ Err(_) => return Value::Bool(false),
402
+ };
403
+ g.remove(&id)
404
+ };
405
+ match sess {
406
+ Some(s) => {
407
+ if let Ok(mut c) = s.child.lock() {
408
+ let _ = c.kill();
409
+ let _ = c.wait();
410
+ }
411
+ Value::Bool(true)
412
+ }
413
+ None => Value::Bool(false),
414
+ }
415
+ }
416
+
417
+ /// `pid(id)` → the child process id, or `null` for an unknown id.
418
+ pub fn process_pid(args: &[Value]) -> Value {
419
+ let id = match arg_u64(args, 0) {
420
+ Some(x) => x,
421
+ None => return Value::Null,
422
+ };
423
+ let g = match sessions().lock() {
424
+ Ok(g) => g,
425
+ Err(_) => return Value::Null,
426
+ };
427
+ match g.get(&id).and_then(|s| s.pid) {
428
+ Some(p) => Value::Number(p as f64),
429
+ None => Value::Null,
430
+ }
431
+ }
432
+
433
+ #[cfg(all(test, unix))]
434
+ mod tests {
435
+ use super::*;
436
+
437
+ fn spawn_cat() -> f64 {
438
+ // `cat` echoes stdin → stdout, a clean full-duplex pipe streaming target.
439
+ let mut m = tishlang_core::ObjectMap::default();
440
+ m.insert(std::sync::Arc::from("program"), Value::String("cat".into()));
441
+ match process_spawn(&[Value::object(m)]) {
442
+ Value::Number(n) => n,
443
+ other => panic!("spawn failed: {:?}", other),
444
+ }
445
+ }
446
+
447
+ #[test]
448
+ fn spawn_write_read_roundtrip() {
449
+ let id = spawn_cat();
450
+ assert!(matches!(
451
+ process_write_stdin(&[Value::Number(id), Value::String("hello_pipe_42\n".into())]),
452
+ Value::Bool(true)
453
+ ));
454
+ let mut acc = String::new();
455
+ for _ in 0..100 {
456
+ match process_read_stdout(&[Value::Number(id), Value::Number(50.0)]) {
457
+ Value::String(s) => acc.push_str(&s),
458
+ Value::Null => break,
459
+ _ => {}
460
+ }
461
+ if acc.contains("hello_pipe_42") {
462
+ break;
463
+ }
464
+ }
465
+ assert!(matches!(process_pid(&[Value::Number(id)]), Value::Number(_)));
466
+ assert!(matches!(process_kill(&[Value::Number(id)]), Value::Bool(true)));
467
+ assert!(acc.contains("hello_pipe_42"), "pipe output missing: {:?}", acc);
468
+ }
469
+
470
+ #[test]
471
+ fn wait_reports_exit_code() {
472
+ let mut m = tishlang_core::ObjectMap::default();
473
+ m.insert(std::sync::Arc::from("program"), Value::String("sh".into()));
474
+ let mut args = Vec::new();
475
+ args.push(Value::String("-c".into()));
476
+ args.push(Value::String("exit 7".into()));
477
+ m.insert(
478
+ std::sync::Arc::from("args"),
479
+ Value::Array(tishlang_core::VmRef::new(args)),
480
+ );
481
+ let id = match process_spawn(&[Value::object(m)]) {
482
+ Value::Number(n) => n,
483
+ other => panic!("spawn failed: {:?}", other),
484
+ };
485
+ let mut code = Value::Null;
486
+ for _ in 0..100 {
487
+ code = process_wait(&[Value::Number(id), Value::Number(50.0)]);
488
+ if !matches!(code, Value::Null) {
489
+ break;
490
+ }
491
+ }
492
+ assert!(matches!(code, Value::Number(n) if n == 7.0), "exit code: {:?}", code);
493
+ let _ = process_kill(&[Value::Number(id)]);
494
+ }
495
+
496
+ #[test]
497
+ fn unknown_id_surfaces_null_or_false() {
498
+ let bad = 9_999_999.0;
499
+ assert!(matches!(process_read_stdout(&[Value::Number(bad)]), Value::Null));
500
+ assert!(matches!(
501
+ process_write_stdin(&[Value::Number(bad), Value::String("x".into())]),
502
+ Value::Bool(false)
503
+ ));
504
+ assert!(matches!(process_wait(&[Value::Number(bad)]), Value::Null));
505
+ assert!(matches!(process_kill(&[Value::Number(bad)]), Value::Bool(false)));
506
+ assert!(matches!(process_pid(&[Value::Number(bad)]), Value::Null));
507
+ }
508
+ }