@tishlang/tish 1.0.28 → 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 (65) 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 +8 -55
  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 -1
  19. package/crates/tish_compile_js/src/codegen.rs +43 -147
  20. package/crates/tish_compile_js/src/lib.rs +4 -7
  21. package/crates/tish_compile_js/src/tests_jsx.rs +89 -19
  22. package/crates/tish_compiler_wasm/src/lib.rs +2 -3
  23. package/crates/tish_core/Cargo.toml +4 -0
  24. package/crates/tish_core/src/console_style.rs +7 -1
  25. package/crates/tish_core/src/json.rs +1 -2
  26. package/crates/tish_core/src/macros.rs +2 -3
  27. package/crates/tish_core/src/value.rs +10 -5
  28. package/crates/tish_eval/Cargo.toml +2 -0
  29. package/crates/tish_eval/src/eval.rs +149 -72
  30. package/crates/tish_eval/src/http.rs +3 -4
  31. package/crates/tish_eval/src/regex.rs +3 -2
  32. package/crates/tish_eval/src/value.rs +11 -13
  33. package/crates/tish_eval/src/value_convert.rs +4 -8
  34. package/crates/tish_fmt/src/lib.rs +49 -10
  35. package/crates/tish_jsx_web/Cargo.toml +1 -1
  36. package/crates/tish_jsx_web/README.md +3 -16
  37. package/crates/tish_jsx_web/src/lib.rs +2 -157
  38. package/crates/tish_lexer/src/token.rs +2 -0
  39. package/crates/tish_lint/src/lib.rs +9 -0
  40. package/crates/tish_lsp/README.md +1 -1
  41. package/crates/tish_native/src/build.rs +16 -2
  42. package/crates/tish_opt/src/lib.rs +15 -0
  43. package/crates/tish_parser/src/lib.rs +101 -1
  44. package/crates/tish_parser/src/parser.rs +161 -50
  45. package/crates/tish_runtime/src/http.rs +4 -5
  46. package/crates/tish_runtime/src/http_fetch.rs +9 -10
  47. package/crates/tish_runtime/src/lib.rs +9 -2
  48. package/crates/tish_runtime/src/promise.rs +2 -3
  49. package/crates/tish_runtime/src/promise_io.rs +2 -3
  50. package/crates/tish_runtime/src/ws.rs +7 -7
  51. package/crates/tish_ui/Cargo.toml +17 -0
  52. package/crates/tish_ui/src/jsx.rs +390 -0
  53. package/crates/tish_ui/src/lib.rs +16 -0
  54. package/crates/tish_ui/src/runtime/hooks.rs +122 -0
  55. package/crates/tish_ui/src/runtime/mod.rs +173 -0
  56. package/crates/tish_vm/src/vm.rs +121 -27
  57. package/justfile +3 -3
  58. package/package.json +1 -1
  59. package/platform/darwin-arm64/tish +0 -0
  60. package/platform/darwin-x64/tish +0 -0
  61. package/platform/linux-arm64/tish +0 -0
  62. package/platform/linux-x64/tish +0 -0
  63. package/platform/win32-x64/tish.exe +0 -0
  64. package/crates/tish_compile_js/src/js_intrinsics.rs +0 -82
  65. package/crates/tish_jsx_web/vendor/Lattish.tish +0 -362
@@ -6,13 +6,15 @@
6
6
  //! different shape (no AST-carrying variants). The split is intentional.
7
7
 
8
8
  use std::cell::RefCell;
9
- use std::collections::HashMap;
10
9
  use std::rc::Rc;
11
10
  use std::sync::Arc;
12
11
 
13
- use tishlang_ast::{Expr, Statement};
14
- #[cfg(any(feature = "http", feature = "ws"))]
12
+ use ahash::AHashMap;
13
+ use tishlang_ast::{FunParam, Statement};
15
14
  use tishlang_core::NativeFn as CoreNativeFn;
15
+
16
+ /// Property map for interpreter `Value::Object` (uses `eval::Value`, not `tishlang_core::Value`).
17
+ pub type PropMap = AHashMap<Arc<str>, Value>;
16
18
  #[cfg(feature = "http")]
17
19
  use tishlang_core::TishPromise;
18
20
  use tishlang_core::TishOpaque;
@@ -32,11 +34,10 @@ pub enum Value {
32
34
  Bool(bool),
33
35
  Null,
34
36
  Array(Rc<RefCell<Vec<Value>>>),
35
- Object(Rc<RefCell<HashMap<Arc<str>, Value>>>),
37
+ Object(Rc<RefCell<PropMap>>),
36
38
  /// User-defined function with AST body
37
39
  Function {
38
- params: Arc<[Arc<str>]>,
39
- defaults: Arc<[Option<Expr>]>,
40
+ formals: Arc<[FunParam]>,
40
41
  rest_param: Option<Arc<str>>,
41
42
  body: Arc<Statement>,
42
43
  },
@@ -65,8 +66,7 @@ pub enum Value {
65
66
  /// Native `tishlang_core` Promise (fetch / reader.read / response.text).
66
67
  #[cfg(feature = "http")]
67
68
  CorePromise(Arc<dyn TishPromise>),
68
- /// `tishlang_core::Value::Function` (e.g. response.text/json, ws send/receive) callable from eval.
69
- #[cfg(any(feature = "http", feature = "ws"))]
69
+ /// `tishlang_core::Value::Function` (native callbacks, `new` constructors, fetch/ws when enabled).
70
70
  CoreFn(CoreNativeFn),
71
71
  /// Opaque handle to a native Rust type (e.g. Polars DataFrame).
72
72
  Opaque(Arc<dyn TishOpaque>),
@@ -99,7 +99,6 @@ impl std::fmt::Debug for Value {
99
99
  Value::BoundPromiseMethod(_, _) | Value::TimerBuiltin(_) => write!(f, "[Function]"),
100
100
  #[cfg(feature = "http")]
101
101
  Value::CorePromise(_) => write!(f, "Promise"),
102
- #[cfg(any(feature = "http", feature = "ws"))]
103
102
  Value::CoreFn(_) => write!(f, "CoreFn"),
104
103
  Value::Opaque(o) => write!(f, "{}(opaque)", o.type_name()),
105
104
  Value::OpaqueMethod(_, _) => write!(f, "[Function]"),
@@ -155,7 +154,6 @@ impl std::fmt::Display for Value {
155
154
  Value::BoundPromiseMethod(_, _) | Value::TimerBuiltin(_) => write!(f, "[Function]"),
156
155
  #[cfg(feature = "http")]
157
156
  Value::CorePromise(_) => write!(f, "[Promise]"),
158
- #[cfg(any(feature = "http", feature = "ws"))]
159
157
  Value::CoreFn(_) => write!(f, "[Function]"),
160
158
  Value::Opaque(o) => write!(f, "[object {}]", o.type_name()),
161
159
  Value::OpaqueMethod(_, _) => write!(f, "[Function]"),
@@ -199,8 +197,8 @@ impl Value {
199
197
  Value::Array(Rc::new(RefCell::new(items)))
200
198
  }
201
199
 
202
- /// Create a new object Value from a HashMap.
203
- pub fn object(map: HashMap<Arc<str>, Value>) -> Self {
200
+ /// Create a new object Value from a property map.
201
+ pub fn object(map: PropMap) -> Self {
204
202
  Value::Object(Rc::new(RefCell::new(map)))
205
203
  }
206
204
 
@@ -211,7 +209,7 @@ impl Value {
211
209
 
212
210
  /// Create an empty object Value.
213
211
  pub fn empty_object() -> Self {
214
- Value::Object(Rc::new(RefCell::new(HashMap::new())))
212
+ Value::Object(Rc::new(RefCell::new(PropMap::default())))
215
213
  }
216
214
 
217
215
  /// Extract the number value, if this is a Number.
@@ -1,13 +1,12 @@
1
1
  //! Conversion between tishlang_eval::Value and tishlang_core::Value for opaque method calls.
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
 
8
- use tishlang_core::Value as CoreValue;
7
+ use tishlang_core::{ObjectMap, Value as CoreValue};
9
8
 
10
- use crate::value::Value;
9
+ use crate::value::{PropMap, Value};
11
10
 
12
11
  /// Convert interpreter Value to core Value. Fails for interpreter-only variants.
13
12
  pub fn eval_to_core(v: &Value) -> Result<CoreValue, String> {
@@ -24,7 +23,7 @@ pub fn eval_to_core(v: &Value) -> Result<CoreValue, String> {
24
23
  Ok(CoreValue::Array(Rc::new(RefCell::new(out))))
25
24
  }
26
25
  Value::Object(map) => {
27
- let mut out = HashMap::new();
26
+ let mut out = ObjectMap::default();
28
27
  for (k, v) in map.borrow().iter() {
29
28
  out.insert(Arc::clone(k), eval_to_core(v)?);
30
29
  }
@@ -53,7 +52,7 @@ pub fn core_to_eval(v: CoreValue) -> Value {
53
52
  Value::Array(Rc::new(RefCell::new(out)))
54
53
  }
55
54
  CoreValue::Object(map) => {
56
- let mut out = HashMap::new();
55
+ let mut out = PropMap::default();
57
56
  for (k, v) in map.borrow().iter() {
58
57
  out.insert(Arc::clone(k), core_to_eval(v.clone()));
59
58
  }
@@ -64,10 +63,7 @@ pub fn core_to_eval(v: CoreValue) -> Value {
64
63
  CoreValue::Promise(p) => Value::CorePromise(Arc::clone(&p)),
65
64
  #[cfg(not(feature = "http"))]
66
65
  CoreValue::Promise(_) => Value::Null,
67
- #[cfg(any(feature = "http", feature = "ws"))]
68
66
  CoreValue::Function(f) => Value::CoreFn(Rc::clone(&f)),
69
- #[cfg(not(any(feature = "http", feature = "ws")))]
70
- CoreValue::Function(_) => Value::Null,
71
67
  // tishlang_core gets regex from http or regex features; handle RegExp when it exists
72
68
  #[cfg(any(feature = "http", feature = "regex"))]
73
69
  CoreValue::RegExp(re) => {
@@ -2,7 +2,7 @@
2
2
 
3
3
  use tishlang_ast::{
4
4
  ArrayElement, ArrowBody, BinOp, CallArg, CompoundOp, DestructElement, DestructPattern,
5
- ExportDeclaration, Expr, ImportSpecifier, JsxAttrValue, JsxChild, JsxProp,
5
+ ExportDeclaration, Expr, FunParam, ImportSpecifier, JsxAttrValue, JsxChild, JsxProp,
6
6
  Literal, LogicalAssignOp, MemberProp, ObjectProp, Program, Statement, TypeAnnotation,
7
7
  TypedParam, UnaryOp,
8
8
  };
@@ -385,19 +385,38 @@ impl Printer {
385
385
  self.buf.push_str(" }");
386
386
  }
387
387
 
388
- fn param_list(&mut self, params: &[TypedParam], rest: &Option<TypedParam>) {
388
+ fn param_list(&mut self, params: &[FunParam], rest: &Option<TypedParam>) {
389
389
  for (i, p) in params.iter().enumerate() {
390
390
  if i > 0 {
391
391
  self.buf.push_str(", ");
392
392
  }
393
- self.buf.push_str(p.name.as_ref());
394
- if let Some(t) = &p.type_ann {
395
- self.buf.push_str(": ");
396
- self.type_ann(t);
397
- }
398
- if let Some(e) = &p.default {
399
- self.buf.push_str(" = ");
400
- self.expr(e);
393
+ match p {
394
+ FunParam::Simple(tp) => {
395
+ self.buf.push_str(tp.name.as_ref());
396
+ if let Some(t) = &tp.type_ann {
397
+ self.buf.push_str(": ");
398
+ self.type_ann(t);
399
+ }
400
+ if let Some(e) = &tp.default {
401
+ self.buf.push_str(" = ");
402
+ self.expr(e);
403
+ }
404
+ }
405
+ FunParam::Destructure {
406
+ pattern,
407
+ type_ann,
408
+ default,
409
+ } => {
410
+ self.destruct_pat(pattern);
411
+ if let Some(t) = type_ann {
412
+ self.buf.push_str(": ");
413
+ self.type_ann(t);
414
+ }
415
+ if let Some(e) = default {
416
+ self.buf.push_str(" = ");
417
+ self.expr(e);
418
+ }
419
+ }
401
420
  }
402
421
  }
403
422
  if let Some(r) = rest {
@@ -551,6 +570,26 @@ impl Printer {
551
570
  }
552
571
  self.buf.push(')');
553
572
  }
573
+ Expr::New { callee, args, .. } => {
574
+ self.buf.push_str("new ");
575
+ self.expr(callee);
576
+ if !args.is_empty() {
577
+ self.buf.push('(');
578
+ for (i, a) in args.iter().enumerate() {
579
+ if i > 0 {
580
+ self.buf.push_str(", ");
581
+ }
582
+ match a {
583
+ CallArg::Expr(ex) => self.expr(ex),
584
+ CallArg::Spread(ex) => {
585
+ self.buf.push_str("...");
586
+ self.expr(ex);
587
+ }
588
+ }
589
+ }
590
+ self.buf.push(')');
591
+ }
592
+ }
554
593
  Expr::Member {
555
594
  object,
556
595
  prop,
@@ -2,7 +2,7 @@
2
2
  name = "tishlang_jsx_web"
3
3
  version = "0.1.0"
4
4
  edition = "2021"
5
- description = "JSX / DOM runtime snippets for Tish JS target only (not used by native)"
5
+ description = "Workspace placeholder; Lattish runtime is the lattish npm/git repo (not vendored here)"
6
6
 
7
7
  license-file = { workspace = true }
8
8
  repository = { workspace = true }
@@ -1,18 +1,5 @@
1
- # tish_jsx_web
1
+ # tishlang_jsx_web
2
2
 
3
- Web-only crate for the Tish JS backend:
3
+ Workspace placeholder. **Lattish** (`h`, `Fragment`, hooks, reconciler) lives in the [lattish](https://github.com/tishlang/lattish) repository — edit **`lattish/src/Lattish.tish` only**.
4
4
 
5
- - **`VDOM_PRELUDE`** vnode helpers + `window.__lattishVdomPatch` + `window.__LATTISH_JSX_VDOM` for `--jsx vdom`.
6
-
7
- Native / non-JS compiler targets must not depend on this crate; only `tish_compile_js` pulls it in.
8
-
9
- ## Vendor runtime
10
-
11
- `vendor/Lattish.tish` is a copy refreshed from the **lattish** npm package. Run `just refresh-lattish` to update from the sibling lattish package.
12
-
13
- ## JSX modes (`--jsx`)
14
-
15
- | Mode | JSX lowers to | Preamble |
16
- |------|----------------|----------|
17
- | `lattish` (default) | Lattish-style JSX lowering | none — merge `Lattish.tish` by importing any export you need (or `import {} from "lattish"` for JSX-only) |
18
- | `vdom` | `__vdom_h(...)` | VDOM prelude; Lattish `createRoot` patches the tree when `window.__LATTISH_JSX_VDOM` is set |
5
+ Tish lowers JSX to `h` / `Fragment` via `tishlang_compile_js`; apps merge a compiled `Lattish.js` from npm or a path dependency.
@@ -1,157 +1,2 @@
1
- //! Web-only JSX helpers for `tish compile --target js`.
2
- //! Native and WASM native paths must not depend on this crate.
3
-
4
- /// VDOM: vnode `__vdom_h` + `window.__lattishVdomPatch` for Lattish batched flush.
5
- pub const VDOM_PRELUDE: &str = r#"window.__LATTISH_JSX_VDOM = true;
6
- const __Fragment = Symbol('Fragment');
7
- function __vdom_h(tag, props, children) {
8
- if (children === undefined || children === null) children = [];
9
- if (!Array.isArray(children)) children = [children];
10
- return { tag: tag, props: props || null, children: children, _el: null };
11
- }
12
- function __vdom_flatten(ch) {
13
- const out = [];
14
- function w(c) {
15
- if (c == null) return;
16
- if (Array.isArray(c)) { for (let i = 0; i < c.length; i++) w(c[i]); return; }
17
- if (typeof c === 'object' && c && c.tag === __Fragment) {
18
- const inner = c.children;
19
- if (Array.isArray(inner)) for (let i = 0; i < inner.length; i++) w(inner[i]);
20
- return;
21
- }
22
- out.push(c);
23
- }
24
- if (Array.isArray(ch)) for (let i = 0; i < ch.length; i++) w(ch[i]);
25
- return out;
26
- }
27
- function __vdom_mount(v) {
28
- if (typeof v === 'string') return document.createTextNode(v);
29
- if (v.tag === __Fragment) {
30
- const f = document.createDocumentFragment();
31
- const ch = __vdom_flatten(v.children);
32
- for (let i = 0; i < ch.length; i++) f.appendChild(__vdom_mount(ch[i]));
33
- return f;
34
- }
35
- const el = document.createElement(v.tag);
36
- const p = v.props || {};
37
- for (const k of Object.keys(p)) {
38
- const val = p[k];
39
- if (val === true) el.setAttribute(k, k);
40
- else if (val !== false && val != null) {
41
- if (k === 'class' || k === 'className') el.className = val;
42
- else if (k.startsWith('on') && typeof val === 'function') el[k.toLowerCase()] = val;
43
- else if (k === 'value' && (v.tag === 'input' || v.tag === 'textarea' || v.tag === 'select')) el.value = val;
44
- else el.setAttribute(k, String(val));
45
- }
46
- }
47
- const ch = __vdom_flatten(v.children);
48
- for (let i = 0; i < ch.length; i++) el.appendChild(__vdom_mount(ch[i]));
49
- v._el = el;
50
- return el;
51
- }
52
- function __vdomPatchEl(el, ov, nv) {
53
- if (!el || !ov || !nv) return false;
54
- if (typeof nv === 'string') {
55
- if (el.nodeType === 3) el.textContent = nv;
56
- return true;
57
- }
58
- if (ov.tag !== nv.tag) return false;
59
- nv._el = el;
60
- const p = nv.props || {};
61
- const op = ov.props || {};
62
- for (const k of Object.keys(p)) {
63
- if (p[k] !== op[k]) {
64
- const val = p[k];
65
- if (k === 'class' || k === 'className') el.className = val || '';
66
- else if (k.startsWith('on')) el[k.toLowerCase()] = val || null;
67
- else if (k === 'value' && (nv.tag === 'input' || nv.tag === 'textarea' || nv.tag === 'select')) {
68
- if (el !== document.activeElement) el.value = val;
69
- } else el.setAttribute(k, String(val));
70
- }
71
- }
72
- const ocx = __vdom_flatten(ov.children);
73
- const ncx = __vdom_flatten(nv.children);
74
- let fullCh = ocx.length !== ncx.length;
75
- if (!fullCh) {
76
- for (let j = 0; j < ncx.length; j++) {
77
- const o = ocx[j], n = ncx[j];
78
- if (typeof n === 'string') { if (typeof o !== 'string') fullCh = true; }
79
- else if (typeof o === 'string') fullCh = true;
80
- else if (!o || o.tag !== n.tag) fullCh = true;
81
- if (fullCh) break;
82
- }
83
- }
84
- if (fullCh) {
85
- const nodes = [];
86
- for (let j = 0; j < ncx.length; j++) nodes.push(__vdom_mount(ncx[j]));
87
- el.replaceChildren(...nodes);
88
- return true;
89
- }
90
- let i = 0;
91
- while (i < ncx.length) {
92
- const o = ocx[i], n = ncx[i];
93
- const childEl = el.childNodes[i];
94
- if (n == null) {
95
- if (childEl) el.removeChild(childEl);
96
- i++;
97
- continue;
98
- }
99
- if (o == null) {
100
- el.appendChild(__vdom_mount(n));
101
- i++;
102
- continue;
103
- }
104
- if (typeof n === 'string') {
105
- if (childEl && childEl.nodeType === 3) childEl.textContent = n;
106
- else {
107
- const m = __vdom_mount(n);
108
- if (childEl) el.replaceChild(m, childEl);
109
- else el.appendChild(m);
110
- }
111
- i++;
112
- continue;
113
- }
114
- if (typeof o === 'string' || o.tag !== n.tag) {
115
- const m = __vdom_mount(n);
116
- if (childEl) el.replaceChild(m, childEl);
117
- else el.appendChild(m);
118
- i++;
119
- continue;
120
- }
121
- if (!childEl || !__vdomPatchEl(childEl, o, n)) {
122
- const m = __vdom_mount(n);
123
- if (childEl) el.replaceChild(m, childEl);
124
- else el.appendChild(m);
125
- }
126
- i++;
127
- }
128
- return true;
129
- }
130
- window.__lattishVdomPatch = function(container, oldTree, newTree) {
131
- try {
132
- if (oldTree == null) {
133
- container.replaceChildren(__vdom_mount(newTree));
134
- return;
135
- }
136
- const el = container.firstChild;
137
- if (!el) {
138
- container.appendChild(__vdom_mount(newTree));
139
- return;
140
- }
141
- if (typeof newTree === 'string' || oldTree.tag !== newTree.tag) {
142
- container.replaceChildren(__vdom_mount(newTree));
143
- return;
144
- }
145
- if (!__vdomPatchEl(el, oldTree, newTree)) {
146
- container.replaceChildren(__vdom_mount(newTree));
147
- }
148
- } catch (err) {
149
- console.warn('Lattish VDOM patch failed, full remount', err);
150
- try {
151
- container.replaceChildren(__vdom_mount(newTree));
152
- } catch (e2) {
153
- console.error('Lattish VDOM remount failed', e2);
154
- }
155
- }
156
- };
157
- "#;
1
+ //! Placeholder crate kept for workspace / crates.io ordering. The JSX runtime is maintained in the
2
+ //! [lattish](https://github.com/tishlang/lattish) repo (`src/Lattish.tish` only do not vendor a copy here).
@@ -54,6 +54,7 @@ pub enum TokenKind {
54
54
  In,
55
55
  Async,
56
56
  Await,
57
+ New,
57
58
  Import,
58
59
  Export,
59
60
 
@@ -149,6 +150,7 @@ impl TokenKind {
149
150
  "in" => TokenKind::In,
150
151
  "async" => TokenKind::Async,
151
152
  "await" => TokenKind::Await,
153
+ "new" => TokenKind::New,
152
154
  "import" => TokenKind::Import,
153
155
  "export" => TokenKind::Export,
154
156
  _ => TokenKind::Ident,
@@ -180,6 +180,15 @@ fn lint_expr(e: &Expr, out: &mut Vec<LintDiagnostic>) {
180
180
  }
181
181
  }
182
182
  }
183
+ Expr::New { callee, args, .. } => {
184
+ lint_expr(callee, out);
185
+ for a in args {
186
+ match a {
187
+ tishlang_ast::CallArg::Expr(x) => lint_expr(x, out),
188
+ tishlang_ast::CallArg::Spread(x) => lint_expr(x, out),
189
+ }
190
+ }
191
+ }
183
192
  Expr::Member { object, .. } => {
184
193
  lint_expr(object, out);
185
194
  }
@@ -5,7 +5,7 @@ Language Server Protocol implementation for [Tish](https://github.com/tishlang/t
5
5
  ## Build
6
6
 
7
7
  ```bash
8
- cargo build --release -p tish_lsp
8
+ cargo build --release -p tishlang_lsp
9
9
  ```
10
10
 
11
11
  Binary: `target/release/tish-lsp` (stdio LSP).
@@ -45,6 +45,19 @@ pub fn build_via_cargo(
45
45
  })
46
46
  .collect();
47
47
 
48
+ let tish_ui_path = std::path::Path::new(&runtime_path)
49
+ .parent()
50
+ .ok_or_else(|| "invalid tishlang_runtime path (no parent)".to_string())?
51
+ .join("tish_ui");
52
+ let ui_dep = if rust_code.contains("tishlang_ui") {
53
+ format!(
54
+ "\ntishlang_ui = {{ path = {:?}, default-features = false, features = [\"runtime\"] }}\n",
55
+ tish_ui_path.display().to_string().replace('\\', "/")
56
+ )
57
+ } else {
58
+ String::new()
59
+ };
60
+
48
61
  let cargo_toml = format!(
49
62
  r#"[package]
50
63
  name = "tish_output"
@@ -63,7 +76,7 @@ codegen-units = 1
63
76
  lto = "thin"
64
77
 
65
78
  [dependencies]
66
- tishlang_runtime = {{ path = {:?}{} }}{}{}
79
+ tishlang_runtime = {{ path = {:?}{} }}{}{}{}
67
80
  "#,
68
81
  out_name,
69
82
  runtime_path,
@@ -73,7 +86,8 @@ tishlang_runtime = {{ path = {:?}{} }}{}{}
73
86
  String::new()
74
87
  } else {
75
88
  format!("\n{}", native_deps)
76
- }
89
+ },
90
+ ui_dep
77
91
  );
78
92
 
79
93
  fs::write(build_dir.join("Cargo.toml"), cargo_toml)
@@ -339,6 +339,21 @@ fn optimize_expr(expr: &Expr) -> Expr {
339
339
  .collect(),
340
340
  span: *span,
341
341
  },
342
+ Expr::New {
343
+ callee,
344
+ args,
345
+ span,
346
+ } => Expr::New {
347
+ callee: Box::new(optimize_expr(callee)),
348
+ args: args
349
+ .iter()
350
+ .map(|a| match a {
351
+ tishlang_ast::CallArg::Expr(e) => tishlang_ast::CallArg::Expr(optimize_expr(e)),
352
+ tishlang_ast::CallArg::Spread(e) => tishlang_ast::CallArg::Spread(optimize_expr(e)),
353
+ })
354
+ .collect(),
355
+ span: *span,
356
+ },
342
357
  Expr::Member {
343
358
  object,
344
359
  prop,
@@ -18,7 +18,7 @@ pub fn parse(source: &str) -> Result<Program, String> {
18
18
  #[cfg(test)]
19
19
  mod tests {
20
20
  use super::*;
21
- use tishlang_ast::{Expr, ObjectProp, Statement};
21
+ use tishlang_ast::{CallArg, Expr, ObjectProp, Statement};
22
22
 
23
23
  #[test]
24
24
  fn test_async_fn_parse() {
@@ -120,4 +120,104 @@ mod tests {
120
120
  _ => panic!("expected KeyValue prop"),
121
121
  }
122
122
  }
123
+
124
+ fn unwrap_expr_stmt(program: &tishlang_ast::Program) -> &Expr {
125
+ match program.statements.first() {
126
+ Some(Statement::ExprStmt { expr, .. }) => expr,
127
+ _ => panic!("expected expression statement"),
128
+ }
129
+ }
130
+
131
+ #[test]
132
+ fn new_expression_simple_call() {
133
+ let program = parse("new Foo()").expect("parse");
134
+ let e = unwrap_expr_stmt(&program);
135
+ match e {
136
+ Expr::New { callee, args, .. } => {
137
+ assert!(matches!(callee.as_ref(), Expr::Ident { name, .. } if name.as_ref() == "Foo"));
138
+ assert!(args.is_empty());
139
+ }
140
+ _ => panic!("expected New, got {:?}", e),
141
+ }
142
+ }
143
+
144
+ #[test]
145
+ fn new_expression_with_args() {
146
+ let program = parse("new Uint8Array(16)").expect("parse");
147
+ let e = unwrap_expr_stmt(&program);
148
+ match e {
149
+ Expr::New { callee, args, .. } => {
150
+ assert!(matches!(callee.as_ref(), Expr::Ident { name, .. } if name.as_ref() == "Uint8Array"));
151
+ assert_eq!(args.len(), 1);
152
+ assert!(matches!(&args[0], CallArg::Expr(Expr::Literal { .. })));
153
+ }
154
+ _ => panic!("expected New"),
155
+ }
156
+ }
157
+
158
+ #[test]
159
+ fn new_expression_member_callee() {
160
+ let program = parse("new ns.AudioContext()").expect("parse");
161
+ let e = unwrap_expr_stmt(&program);
162
+ match e {
163
+ Expr::New { callee, args, .. } => {
164
+ assert!(matches!(
165
+ callee.as_ref(),
166
+ Expr::Member { prop: tishlang_ast::MemberProp::Name(p), .. } if p.as_ref() == "AudioContext"
167
+ ));
168
+ assert!(args.is_empty());
169
+ }
170
+ _ => panic!("expected New"),
171
+ }
172
+ }
173
+
174
+ #[test]
175
+ fn new_expression_chained_new() {
176
+ let program = parse("new new Date()").expect("parse");
177
+ let e = unwrap_expr_stmt(&program);
178
+ match e {
179
+ Expr::New { callee, args, .. } => {
180
+ assert!(args.is_empty());
181
+ match callee.as_ref() {
182
+ Expr::New { callee: inner, args: inner_args, .. } => {
183
+ assert!(matches!(inner.as_ref(), Expr::Ident { name, .. } if name.as_ref() == "Date"));
184
+ assert!(inner_args.is_empty());
185
+ }
186
+ _ => panic!("expected nested New"),
187
+ }
188
+ }
189
+ _ => panic!("expected New"),
190
+ }
191
+ }
192
+
193
+ #[test]
194
+ fn new_then_member_access() {
195
+ let program = parse("new Foo().bar").expect("parse");
196
+ let e = unwrap_expr_stmt(&program);
197
+ match e {
198
+ Expr::Member { object, prop: tishlang_ast::MemberProp::Name(p), .. } => {
199
+ assert_eq!(p.as_ref(), "bar");
200
+ match object.as_ref() {
201
+ Expr::New { callee, args, .. } => {
202
+ assert!(matches!(callee.as_ref(), Expr::Ident { name, .. } if name.as_ref() == "Foo"));
203
+ assert!(args.is_empty());
204
+ }
205
+ _ => panic!("expected New object"),
206
+ }
207
+ }
208
+ _ => panic!("expected Member"),
209
+ }
210
+ }
211
+
212
+ #[test]
213
+ fn new_with_spread_arg() {
214
+ let program = parse("new Foo(...xs)").expect("parse");
215
+ let e = unwrap_expr_stmt(&program);
216
+ match e {
217
+ Expr::New { args, .. } => {
218
+ assert!(matches!(&args[0], CallArg::Spread(Expr::Ident { name, .. }) if name.as_ref() == "xs"));
219
+ }
220
+ _ => panic!("expected New"),
221
+ }
222
+ }
123
223
  }