@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.
- package/Cargo.toml +1 -0
- package/crates/js_to_tish/src/transform/expr.rs +15 -6
- package/crates/tish/Cargo.toml +1 -1
- package/crates/tish/src/main.rs +1 -1
- package/crates/tish/tests/integration_test.rs +4 -3
- package/crates/tish_ast/src/ast.rs +65 -2
- package/crates/tish_build_utils/src/lib.rs +10 -2
- package/crates/tish_builtins/src/construct.rs +177 -0
- package/crates/tish_builtins/src/globals.rs +3 -5
- package/crates/tish_builtins/src/helpers.rs +2 -3
- package/crates/tish_builtins/src/lib.rs +1 -0
- package/crates/tish_builtins/src/object.rs +3 -4
- package/crates/tish_bytecode/src/compiler.rs +85 -11
- package/crates/tish_bytecode/src/opcode.rs +7 -3
- package/crates/tish_compile/Cargo.toml +1 -0
- package/crates/tish_compile/src/codegen.rs +233 -71
- package/crates/tish_compile/src/lib.rs +35 -0
- package/crates/tish_compile_js/Cargo.toml +1 -0
- package/crates/tish_compile_js/src/codegen.rs +38 -94
- package/crates/tish_compile_js/src/lib.rs +0 -1
- package/crates/tish_compile_js/src/tests_jsx.rs +68 -0
- package/crates/tish_core/Cargo.toml +4 -0
- package/crates/tish_core/src/console_style.rs +7 -1
- package/crates/tish_core/src/json.rs +1 -2
- package/crates/tish_core/src/macros.rs +2 -3
- package/crates/tish_core/src/value.rs +10 -5
- package/crates/tish_eval/Cargo.toml +2 -0
- package/crates/tish_eval/src/eval.rs +149 -72
- package/crates/tish_eval/src/http.rs +3 -4
- package/crates/tish_eval/src/regex.rs +3 -2
- package/crates/tish_eval/src/value.rs +11 -13
- package/crates/tish_eval/src/value_convert.rs +4 -8
- package/crates/tish_fmt/src/lib.rs +49 -10
- package/crates/tish_lexer/src/token.rs +2 -0
- package/crates/tish_lint/src/lib.rs +9 -0
- package/crates/tish_lsp/README.md +1 -1
- package/crates/tish_native/src/build.rs +16 -2
- package/crates/tish_opt/src/lib.rs +15 -0
- package/crates/tish_parser/src/lib.rs +101 -1
- package/crates/tish_parser/src/parser.rs +161 -50
- package/crates/tish_runtime/src/http.rs +4 -5
- package/crates/tish_runtime/src/http_fetch.rs +9 -10
- package/crates/tish_runtime/src/lib.rs +9 -2
- package/crates/tish_runtime/src/promise.rs +2 -3
- package/crates/tish_runtime/src/promise_io.rs +2 -3
- package/crates/tish_runtime/src/ws.rs +7 -7
- package/crates/tish_ui/Cargo.toml +17 -0
- package/crates/tish_ui/src/jsx.rs +390 -0
- package/crates/tish_ui/src/lib.rs +16 -0
- package/crates/tish_ui/src/runtime/hooks.rs +122 -0
- package/crates/tish_ui/src/runtime/mod.rs +173 -0
- package/crates/tish_vm/src/vm.rs +121 -27
- package/justfile +3 -3
- package/package.json +1 -1
- package/platform/darwin-arm64/tish +0 -0
- package/platform/darwin-x64/tish +0 -0
- package/platform/linux-arm64/tish +0 -0
- package/platform/linux-x64/tish +0 -0
- package/platform/win32-x64/tish.exe +0 -0
- package/crates/tish_compile_js/src/js_intrinsics.rs +0 -82
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
//! Minimal hook state: `useState` + render flush (Lattish-style cursor reset).
|
|
2
|
+
|
|
3
|
+
use std::cell::{Cell, RefCell};
|
|
4
|
+
use std::rc::Rc;
|
|
5
|
+
|
|
6
|
+
use tishlang_core::{ObjectMap, Value};
|
|
7
|
+
|
|
8
|
+
use super::ACTIVE_HOST;
|
|
9
|
+
|
|
10
|
+
thread_local! {
|
|
11
|
+
pub static HOOK: RefCell<HookState> = RefCell::new(HookState::default());
|
|
12
|
+
static IN_FLUSH: Cell<bool> = Cell::new(false);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
#[derive(Default)]
|
|
16
|
+
pub struct HookState {
|
|
17
|
+
pub state_slots: Rc<RefCell<Vec<Value>>>,
|
|
18
|
+
pub cursor: usize,
|
|
19
|
+
pub root_app: Option<Value>,
|
|
20
|
+
pub root_vnode: Option<Value>,
|
|
21
|
+
pub flush_scheduled: bool,
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
impl HookState {
|
|
25
|
+
pub fn reset_for_new_root(&mut self) {
|
|
26
|
+
self.state_slots.borrow_mut().clear();
|
|
27
|
+
self.cursor = 0;
|
|
28
|
+
self.root_vnode = None;
|
|
29
|
+
self.flush_scheduled = false;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/// `useState(initial)` → `[state, setState]` as a Tish array.
|
|
34
|
+
pub fn native_use_state(args: &[Value]) -> Value {
|
|
35
|
+
let initial = args.first().cloned().unwrap_or(Value::Null);
|
|
36
|
+
HOOK.with(|h| {
|
|
37
|
+
let mut st = h.borrow_mut();
|
|
38
|
+
let i = st.cursor;
|
|
39
|
+
st.cursor += 1;
|
|
40
|
+
let slots = Rc::clone(&st.state_slots);
|
|
41
|
+
while i >= slots.borrow().len() {
|
|
42
|
+
slots.borrow_mut().push(initial.clone());
|
|
43
|
+
}
|
|
44
|
+
let current = slots.borrow()[i].clone();
|
|
45
|
+
let idx = i;
|
|
46
|
+
let setter = Value::Function(Rc::new(move |a: &[Value]| {
|
|
47
|
+
let new_v = a.first().cloned().unwrap_or(Value::Null);
|
|
48
|
+
slots.borrow_mut()[idx] = new_v;
|
|
49
|
+
schedule_flush();
|
|
50
|
+
Value::Null
|
|
51
|
+
}));
|
|
52
|
+
Value::Array(Rc::new(RefCell::new(vec![current, setter])))
|
|
53
|
+
})
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/// `createRoot(container)` → `{ render: (App) => { ... } }` (container ignored for headless native).
|
|
57
|
+
pub fn native_create_root(args: &[Value]) -> Value {
|
|
58
|
+
let _container = args.first();
|
|
59
|
+
let render_fn = Value::Function(Rc::new(|app_args: &[Value]| {
|
|
60
|
+
let app = app_args.first().cloned().unwrap_or(Value::Null);
|
|
61
|
+
HOOK.with(|h| {
|
|
62
|
+
let mut st = h.borrow_mut();
|
|
63
|
+
st.reset_for_new_root();
|
|
64
|
+
st.root_app = Some(app);
|
|
65
|
+
st.flush_scheduled = true;
|
|
66
|
+
});
|
|
67
|
+
drain_flush_queue();
|
|
68
|
+
Value::Null
|
|
69
|
+
}));
|
|
70
|
+
Value::Object(Rc::new(RefCell::new(ObjectMap::from([(
|
|
71
|
+
std::sync::Arc::from("render"),
|
|
72
|
+
render_fn,
|
|
73
|
+
)]))))
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/// Request a re-render (coalesced; safe if called during flush).
|
|
77
|
+
pub fn schedule_flush() {
|
|
78
|
+
HOOK.with(|h| {
|
|
79
|
+
h.borrow_mut().flush_scheduled = true;
|
|
80
|
+
});
|
|
81
|
+
if IN_FLUSH.get() {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
drain_flush_queue();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
fn drain_flush_queue() {
|
|
88
|
+
loop {
|
|
89
|
+
let run = HOOK.with(|h| {
|
|
90
|
+
let mut st = h.borrow_mut();
|
|
91
|
+
if st.flush_scheduled {
|
|
92
|
+
st.flush_scheduled = false;
|
|
93
|
+
true
|
|
94
|
+
} else {
|
|
95
|
+
false
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
if !run {
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
IN_FLUSH.set(true);
|
|
102
|
+
HOOK.with(|h| {
|
|
103
|
+
let mut st = h.borrow_mut();
|
|
104
|
+
st.cursor = 0;
|
|
105
|
+
let Some(app) = st.root_app.clone() else {
|
|
106
|
+
return;
|
|
107
|
+
};
|
|
108
|
+
let Value::Function(f) = app else {
|
|
109
|
+
return;
|
|
110
|
+
};
|
|
111
|
+
let tree = f(&[]);
|
|
112
|
+
st.root_vnode = Some(tree.clone());
|
|
113
|
+
ACTIVE_HOST.with(|host_cell| {
|
|
114
|
+
let mut host_opt = host_cell.borrow_mut();
|
|
115
|
+
if let Some(host) = host_opt.as_deref_mut() {
|
|
116
|
+
host.commit_root(&tree);
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
IN_FLUSH.set(false);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
//! UI runtime: `h`, `Fragment`, vnode shapes compatible with Lattish, minimal hooks, and [`Host`].
|
|
2
|
+
|
|
3
|
+
mod hooks;
|
|
4
|
+
|
|
5
|
+
use std::cell::RefCell;
|
|
6
|
+
use std::rc::Rc;
|
|
7
|
+
use std::sync::Arc;
|
|
8
|
+
|
|
9
|
+
pub use hooks::{native_create_root, native_use_state, schedule_flush, HookState, HOOK};
|
|
10
|
+
|
|
11
|
+
use tishlang_core::{ObjectMap, Value};
|
|
12
|
+
|
|
13
|
+
/// Sentinel string for `Fragment` (native). JS/Lattish uses `Symbol`; hosts compare via equality.
|
|
14
|
+
pub const FRAGMENT_SENTINEL: &str = "__tish_ui_Fragment__";
|
|
15
|
+
|
|
16
|
+
/// `Fragment` marker value for `h(Fragment, null, children)`.
|
|
17
|
+
pub fn fragment_value() -> Value {
|
|
18
|
+
Value::String(FRAGMENT_SENTINEL.into())
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/// Returns true if `tag` refers to [`fragment_value`].
|
|
22
|
+
pub fn is_fragment_tag(tag: &Value) -> bool {
|
|
23
|
+
matches!(tag, Value::String(s) if s.as_ref() == FRAGMENT_SENTINEL)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/// `text(s)` helper — returns string as `Value::String` for JSX text nodes.
|
|
27
|
+
pub fn ui_text(args: &[Value]) -> Value {
|
|
28
|
+
let s = args
|
|
29
|
+
.first()
|
|
30
|
+
.map(|v| v.to_display_string())
|
|
31
|
+
.unwrap_or_default();
|
|
32
|
+
Value::String(s.into())
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/// Vnode factory: `h(tag, props, children)` (Lattish-compatible shape).
|
|
36
|
+
pub fn ui_h(args: &[Value]) -> Value {
|
|
37
|
+
let tag = args.get(0).cloned().unwrap_or(Value::Null);
|
|
38
|
+
let props = args.get(1).cloned().unwrap_or(Value::Null);
|
|
39
|
+
let children_arg = args.get(2).cloned().unwrap_or(Value::Null);
|
|
40
|
+
|
|
41
|
+
let children_vec = normalize_children_list(children_arg);
|
|
42
|
+
|
|
43
|
+
if let Value::Function(f) = &tag {
|
|
44
|
+
let mut merged = if matches!(props, Value::Null) {
|
|
45
|
+
ObjectMap::default()
|
|
46
|
+
} else if let Value::Object(obj) = props {
|
|
47
|
+
obj.borrow().clone()
|
|
48
|
+
} else {
|
|
49
|
+
ObjectMap::default()
|
|
50
|
+
};
|
|
51
|
+
if !children_vec.is_empty() {
|
|
52
|
+
merged.insert(
|
|
53
|
+
Arc::from("children"),
|
|
54
|
+
Value::Array(Rc::new(RefCell::new(children_vec.clone()))),
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
return f(&[Value::Object(Rc::new(RefCell::new(merged)))]);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if is_fragment_tag(&tag) {
|
|
61
|
+
return vnode_fragment(children_vec);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
let tag_str: Arc<str> = match tag {
|
|
65
|
+
Value::String(s) => s,
|
|
66
|
+
_ => return Value::Null,
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
vnode_element(tag_str, props, children_vec)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
fn normalize_children_list(children_arg: Value) -> Vec<Value> {
|
|
73
|
+
match children_arg {
|
|
74
|
+
Value::Null => vec![],
|
|
75
|
+
Value::Array(a) => a.borrow().clone(),
|
|
76
|
+
other => vec![other],
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
fn vnode_element(tag: Arc<str>, props: Value, children: Vec<Value>) -> Value {
|
|
81
|
+
let mut m = ObjectMap::default();
|
|
82
|
+
m.insert(Arc::from("tag"), Value::String(tag));
|
|
83
|
+
m.insert(
|
|
84
|
+
Arc::from("props"),
|
|
85
|
+
if matches!(props, Value::Null) {
|
|
86
|
+
Value::Null
|
|
87
|
+
} else {
|
|
88
|
+
props
|
|
89
|
+
},
|
|
90
|
+
);
|
|
91
|
+
m.insert(
|
|
92
|
+
Arc::from("children"),
|
|
93
|
+
Value::Array(Rc::new(RefCell::new(children))),
|
|
94
|
+
);
|
|
95
|
+
m.insert(Arc::from("_el"), Value::Null);
|
|
96
|
+
Value::Object(Rc::new(RefCell::new(m)))
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
fn vnode_fragment(children: Vec<Value>) -> Value {
|
|
100
|
+
let mut m = ObjectMap::default();
|
|
101
|
+
m.insert(Arc::from("tag"), fragment_value());
|
|
102
|
+
m.insert(Arc::from("props"), Value::Null);
|
|
103
|
+
m.insert(
|
|
104
|
+
Arc::from("children"),
|
|
105
|
+
Value::Array(Rc::new(RefCell::new(children))),
|
|
106
|
+
);
|
|
107
|
+
m.insert(Arc::from("_el"), Value::Null);
|
|
108
|
+
Value::Object(Rc::new(RefCell::new(m)))
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/// Pluggable UI backend (Floem, DOM, SwiftUI, …). Main-thread / single-threaded by default.
|
|
112
|
+
pub trait Host {
|
|
113
|
+
/// Apply a new root vnode (after each render flush).
|
|
114
|
+
fn commit_root(&mut self, vnode: &Value);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/// No-op / test host that only stores the last committed tree.
|
|
118
|
+
pub struct HeadlessHost {
|
|
119
|
+
pub last: Option<Value>,
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
impl Default for HeadlessHost {
|
|
123
|
+
fn default() -> Self {
|
|
124
|
+
Self { last: None }
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
impl Host for HeadlessHost {
|
|
129
|
+
fn commit_root(&mut self, vnode: &Value) {
|
|
130
|
+
self.last = Some(vnode.clone());
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
thread_local! {
|
|
135
|
+
static ACTIVE_HOST: RefCell<Option<Box<dyn Host>>> = RefCell::new(None);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/// Install the thread-local host used by [`schedule_flush`] / `createRoot`.
|
|
139
|
+
pub fn install_thread_local_host(host: Box<dyn Host>) {
|
|
140
|
+
ACTIVE_HOST.with(|c| {
|
|
141
|
+
*c.borrow_mut() = Some(host);
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
pub fn with_thread_local_host<R>(f: impl FnOnce(&mut dyn Host) -> R) -> Option<R> {
|
|
146
|
+
ACTIVE_HOST.with(|c| {
|
|
147
|
+
let mut opt = c.borrow_mut();
|
|
148
|
+
match opt.as_deref_mut() {
|
|
149
|
+
Some(host) => Some(f(host)),
|
|
150
|
+
None => None,
|
|
151
|
+
}
|
|
152
|
+
})
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/// Tag registry hook for future host-specific intrinsic mapping (HTML tag → component kind).
|
|
156
|
+
#[derive(Default)]
|
|
157
|
+
pub struct TagRegistry;
|
|
158
|
+
|
|
159
|
+
impl TagRegistry {
|
|
160
|
+
pub fn new() -> Self {
|
|
161
|
+
Self
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/// Placeholder for subset CSS / style object interpretation.
|
|
166
|
+
#[derive(Default)]
|
|
167
|
+
pub struct StyleInterpreter;
|
|
168
|
+
|
|
169
|
+
impl StyleInterpreter {
|
|
170
|
+
pub fn new() -> Self {
|
|
171
|
+
Self
|
|
172
|
+
}
|
|
173
|
+
}
|
package/crates/tish_vm/src/vm.rs
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
//! Stack-based bytecode VM.
|
|
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
7
|
use tishlang_ast::{BinOp, UnaryOp};
|
|
9
8
|
use tishlang_builtins::array as arr_builtins;
|
|
9
|
+
use tishlang_builtins::construct as construct_builtin;
|
|
10
10
|
use tishlang_builtins::string as str_builtins;
|
|
11
11
|
use tishlang_builtins::globals as globals_builtins;
|
|
12
12
|
use tishlang_builtins::math as math_builtins;
|
|
13
13
|
use tishlang_bytecode::{u8_to_binop, u8_to_unaryop, Chunk, Constant, Opcode, NO_REST_PARAM};
|
|
14
|
-
use tishlang_core::Value;
|
|
14
|
+
use tishlang_core::{ObjectMap, Value};
|
|
15
15
|
|
|
16
16
|
type ArrayMethodFn = Rc<dyn Fn(&[Value]) -> Value>;
|
|
17
17
|
|
|
@@ -69,7 +69,7 @@ fn get_builtin_export(spec: &str, export_name: &str) -> Option<Value> {
|
|
|
69
69
|
.collect(),
|
|
70
70
|
)))),
|
|
71
71
|
"process" => {
|
|
72
|
-
let mut m =
|
|
72
|
+
let mut m = ObjectMap::default();
|
|
73
73
|
m.insert("exit".into(), Value::Function(Rc::new(|args: &[Value]| tishlang_runtime::process_exit(args))));
|
|
74
74
|
m.insert("cwd".into(), Value::Function(Rc::new(|args: &[Value]| tishlang_runtime::process_cwd(args))));
|
|
75
75
|
m.insert("exec".into(), Value::Function(Rc::new(|args: &[Value]| tishlang_runtime::process_exec(args))));
|
|
@@ -145,10 +145,10 @@ fn vm_log_err(s: &str) {
|
|
|
145
145
|
}
|
|
146
146
|
|
|
147
147
|
/// Initialize default globals (console, Math, JSON, etc.)
|
|
148
|
-
fn init_globals() ->
|
|
149
|
-
let mut g =
|
|
148
|
+
fn init_globals() -> ObjectMap {
|
|
149
|
+
let mut g = ObjectMap::default();
|
|
150
150
|
|
|
151
|
-
let mut console =
|
|
151
|
+
let mut console = ObjectMap::default();
|
|
152
152
|
console.insert(
|
|
153
153
|
"debug".into(),
|
|
154
154
|
Value::Function(Rc::new(|args: &[Value]| {
|
|
@@ -191,7 +191,7 @@ fn init_globals() -> HashMap<Arc<str>, Value> {
|
|
|
191
191
|
);
|
|
192
192
|
g.insert("console".into(), Value::Object(Rc::new(RefCell::new(console))));
|
|
193
193
|
|
|
194
|
-
let mut math =
|
|
194
|
+
let mut math = ObjectMap::default();
|
|
195
195
|
math.insert(
|
|
196
196
|
"abs".into(),
|
|
197
197
|
Value::Function(Rc::new(|args: &[Value]| {
|
|
@@ -257,7 +257,7 @@ fn init_globals() -> HashMap<Arc<str>, Value> {
|
|
|
257
257
|
math.insert("E".into(), Value::Number(std::f64::consts::E));
|
|
258
258
|
g.insert("Math".into(), Value::Object(Rc::new(RefCell::new(math))));
|
|
259
259
|
|
|
260
|
-
let mut json =
|
|
260
|
+
let mut json = ObjectMap::default();
|
|
261
261
|
json.insert(
|
|
262
262
|
"parse".into(),
|
|
263
263
|
Value::Function(Rc::new(|args: &[Value]| {
|
|
@@ -292,7 +292,7 @@ fn init_globals() -> HashMap<Arc<str>, Value> {
|
|
|
292
292
|
);
|
|
293
293
|
|
|
294
294
|
// Date - at minimum Date.now() for timing
|
|
295
|
-
let mut date =
|
|
295
|
+
let mut date = ObjectMap::default();
|
|
296
296
|
date.insert(
|
|
297
297
|
"now".into(),
|
|
298
298
|
Value::Function(Rc::new(|_args: &[Value]| {
|
|
@@ -305,8 +305,17 @@ fn init_globals() -> HashMap<Arc<str>, Value> {
|
|
|
305
305
|
);
|
|
306
306
|
g.insert("Date".into(), Value::Object(Rc::new(RefCell::new(date))));
|
|
307
307
|
|
|
308
|
+
g.insert(
|
|
309
|
+
"Uint8Array".into(),
|
|
310
|
+
construct_builtin::uint8_array_constructor_value(),
|
|
311
|
+
);
|
|
312
|
+
g.insert(
|
|
313
|
+
"AudioContext".into(),
|
|
314
|
+
construct_builtin::audio_context_constructor_value(),
|
|
315
|
+
);
|
|
316
|
+
|
|
308
317
|
// Object methods - delegate to tishlang_builtins::globals
|
|
309
|
-
let mut object_methods =
|
|
318
|
+
let mut object_methods = ObjectMap::default();
|
|
310
319
|
object_methods.insert(
|
|
311
320
|
"assign".into(),
|
|
312
321
|
Value::Function(Rc::new(|args: &[Value]| globals_builtins::object_assign(args))),
|
|
@@ -321,7 +330,7 @@ fn init_globals() -> HashMap<Arc<str>, Value> {
|
|
|
321
330
|
g.insert("Object".into(), Value::Object(Rc::new(RefCell::new(object_methods))));
|
|
322
331
|
|
|
323
332
|
// Array.isArray
|
|
324
|
-
let mut array_static =
|
|
333
|
+
let mut array_static = ObjectMap::default();
|
|
325
334
|
array_static.insert(
|
|
326
335
|
"isArray".into(),
|
|
327
336
|
Value::Function(Rc::new(|args: &[Value]| globals_builtins::array_is_array(args))),
|
|
@@ -330,7 +339,7 @@ fn init_globals() -> HashMap<Arc<str>, Value> {
|
|
|
330
339
|
|
|
331
340
|
// String(value) as callable + String.fromCharCode
|
|
332
341
|
let string_convert_fn = Value::Function(Rc::new(|args: &[Value]| globals_builtins::string_convert(args)));
|
|
333
|
-
let mut string_static =
|
|
342
|
+
let mut string_static = ObjectMap::default();
|
|
334
343
|
string_static.insert("fromCharCode".into(), Value::Function(Rc::new(|args: &[Value]| globals_builtins::string_from_char_code(args))));
|
|
335
344
|
string_static.insert(Arc::from("__call"), string_convert_fn);
|
|
336
345
|
g.insert("String".into(), Value::Object(Rc::new(RefCell::new(string_static))));
|
|
@@ -342,12 +351,12 @@ fn init_globals() -> HashMap<Arc<str>, Value> {
|
|
|
342
351
|
);
|
|
343
352
|
g.insert(
|
|
344
353
|
"Fragment".into(),
|
|
345
|
-
Value::Object(Rc::new(RefCell::new(
|
|
354
|
+
Value::Object(Rc::new(RefCell::new(ObjectMap::default()))),
|
|
346
355
|
);
|
|
347
356
|
g.insert(
|
|
348
357
|
"createRoot".into(),
|
|
349
358
|
Value::Function(Rc::new(|_args: &[Value]| {
|
|
350
|
-
let mut render_obj =
|
|
359
|
+
let mut render_obj = ObjectMap::default();
|
|
351
360
|
render_obj.insert(
|
|
352
361
|
"render".into(),
|
|
353
362
|
Value::Function(Rc::new(|_args: &[Value]| Value::Null)),
|
|
@@ -363,13 +372,13 @@ fn init_globals() -> HashMap<Arc<str>, Value> {
|
|
|
363
372
|
Value::Array(Rc::new(RefCell::new(arr)))
|
|
364
373
|
})),
|
|
365
374
|
);
|
|
366
|
-
let mut document_obj =
|
|
375
|
+
let mut document_obj = ObjectMap::default();
|
|
367
376
|
document_obj.insert("body".into(), Value::Null);
|
|
368
377
|
g.insert("document".into(), Value::Object(Rc::new(RefCell::new(document_obj))));
|
|
369
378
|
|
|
370
379
|
#[cfg(feature = "process")]
|
|
371
380
|
{
|
|
372
|
-
let mut process_obj =
|
|
381
|
+
let mut process_obj = ObjectMap::default();
|
|
373
382
|
process_obj.insert(
|
|
374
383
|
"exit".into(),
|
|
375
384
|
Value::Function(Rc::new(|args: &[Value]| tishlang_runtime::process_exit(args))),
|
|
@@ -418,21 +427,21 @@ fn init_globals() -> HashMap<Arc<str>, Value> {
|
|
|
418
427
|
}
|
|
419
428
|
|
|
420
429
|
/// Shared scope for closure capture (parent frame's locals).
|
|
421
|
-
type ScopeMap = Rc<RefCell<
|
|
430
|
+
type ScopeMap = Rc<RefCell<ObjectMap>>;
|
|
422
431
|
|
|
423
432
|
pub struct Vm {
|
|
424
433
|
stack: Vec<Value>,
|
|
425
|
-
scope:
|
|
434
|
+
scope: ObjectMap,
|
|
426
435
|
/// Enclosing scope for closures (captured parent frame locals).
|
|
427
436
|
enclosing: Option<ScopeMap>,
|
|
428
|
-
globals: Rc<RefCell<
|
|
437
|
+
globals: Rc<RefCell<ObjectMap>>,
|
|
429
438
|
}
|
|
430
439
|
|
|
431
440
|
impl Vm {
|
|
432
441
|
pub fn new() -> Self {
|
|
433
442
|
Self {
|
|
434
443
|
stack: Vec::new(),
|
|
435
|
-
scope:
|
|
444
|
+
scope: ObjectMap::default(),
|
|
436
445
|
enclosing: None,
|
|
437
446
|
globals: Rc::new(RefCell::new(init_globals())),
|
|
438
447
|
}
|
|
@@ -477,7 +486,7 @@ impl Vm {
|
|
|
477
486
|
let names = &chunk.names;
|
|
478
487
|
|
|
479
488
|
let mut ip = 0;
|
|
480
|
-
let local_scope: ScopeMap = Rc::new(RefCell::new(
|
|
489
|
+
let local_scope: ScopeMap = Rc::new(RefCell::new(ObjectMap::default()));
|
|
481
490
|
{
|
|
482
491
|
let mut ls = local_scope.borrow_mut();
|
|
483
492
|
let param_count = chunk.param_count as usize;
|
|
@@ -538,7 +547,7 @@ impl Vm {
|
|
|
538
547
|
Value::Function(Rc::new(move |args: &[Value]| {
|
|
539
548
|
let mut vm = Vm {
|
|
540
549
|
stack: Vec::new(),
|
|
541
|
-
scope:
|
|
550
|
+
scope: ObjectMap::default(),
|
|
542
551
|
enclosing: enclosing.clone(),
|
|
543
552
|
globals: Rc::clone(&globals),
|
|
544
553
|
};
|
|
@@ -723,6 +732,45 @@ impl Vm {
|
|
|
723
732
|
let result = f(&args);
|
|
724
733
|
self.stack.push(result);
|
|
725
734
|
}
|
|
735
|
+
Opcode::Construct => {
|
|
736
|
+
let argc = Self::read_u16(code, &mut ip) as usize;
|
|
737
|
+
let mut args = Vec::with_capacity(argc);
|
|
738
|
+
for _ in 0..argc {
|
|
739
|
+
args.push(
|
|
740
|
+
self.stack
|
|
741
|
+
.pop()
|
|
742
|
+
.ok_or_else(|| "Stack underflow in construct".to_string())?,
|
|
743
|
+
);
|
|
744
|
+
}
|
|
745
|
+
args.reverse();
|
|
746
|
+
let callee = self
|
|
747
|
+
.stack
|
|
748
|
+
.pop()
|
|
749
|
+
.ok_or_else(|| "Stack underflow: no callee for construct".to_string())?;
|
|
750
|
+
let result = construct_builtin::construct(&callee, &args);
|
|
751
|
+
self.stack.push(result);
|
|
752
|
+
}
|
|
753
|
+
Opcode::ConstructSpread => {
|
|
754
|
+
let callee = self
|
|
755
|
+
.stack
|
|
756
|
+
.pop()
|
|
757
|
+
.ok_or_else(|| "Stack underflow: callee in ConstructSpread".to_string())?;
|
|
758
|
+
let args_array = self
|
|
759
|
+
.stack
|
|
760
|
+
.pop()
|
|
761
|
+
.ok_or_else(|| "Stack underflow in ConstructSpread".to_string())?;
|
|
762
|
+
let args: Vec<Value> = match &args_array {
|
|
763
|
+
Value::Array(a) => a.borrow().clone(),
|
|
764
|
+
_ => {
|
|
765
|
+
return Err(format!(
|
|
766
|
+
"ConstructSpread: args must be array, got {}",
|
|
767
|
+
args_array.to_display_string()
|
|
768
|
+
));
|
|
769
|
+
}
|
|
770
|
+
};
|
|
771
|
+
let result = construct_builtin::construct(&callee, &args);
|
|
772
|
+
self.stack.push(result);
|
|
773
|
+
}
|
|
726
774
|
Opcode::Return => {
|
|
727
775
|
let v = self.stack.pop().unwrap_or(Value::Null);
|
|
728
776
|
return Ok(v);
|
|
@@ -861,7 +909,7 @@ impl Vm {
|
|
|
861
909
|
}
|
|
862
910
|
Opcode::NewObject => {
|
|
863
911
|
let n = Self::read_u16(code, &mut ip) as usize;
|
|
864
|
-
let mut map =
|
|
912
|
+
let mut map = ObjectMap::with_capacity(n.max(1));
|
|
865
913
|
for _ in 0..n {
|
|
866
914
|
let val = self
|
|
867
915
|
.stack
|
|
@@ -926,7 +974,11 @@ impl Vm {
|
|
|
926
974
|
.stack
|
|
927
975
|
.pop()
|
|
928
976
|
.ok_or_else(|| "Stack underflow".to_string())?;
|
|
929
|
-
let
|
|
977
|
+
let cap = match (&left, &right) {
|
|
978
|
+
(Value::Object(l), Value::Object(r)) => l.borrow().len() + r.borrow().len(),
|
|
979
|
+
_ => 0,
|
|
980
|
+
};
|
|
981
|
+
let mut merged: ObjectMap = ObjectMap::with_capacity(cap.max(8));
|
|
930
982
|
if let Value::Object(l) = &left {
|
|
931
983
|
merged.extend(l.borrow().iter().map(|(k, v)| (Arc::clone(k), v.clone())));
|
|
932
984
|
} else {
|
|
@@ -1112,6 +1164,40 @@ impl Default for Vm {
|
|
|
1112
1164
|
}
|
|
1113
1165
|
}
|
|
1114
1166
|
|
|
1167
|
+
/// Rough byte capacity for string coercion (matches hot paths like `"x" + n + "ms"`).
|
|
1168
|
+
fn estimate_string_concat_len(v: &Value) -> usize {
|
|
1169
|
+
match v {
|
|
1170
|
+
Value::String(s) => s.len(),
|
|
1171
|
+
Value::Number(_) => 24,
|
|
1172
|
+
Value::Bool(_) => 5,
|
|
1173
|
+
Value::Null => 4,
|
|
1174
|
+
_ => 32,
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
/// Append JS-style string conversion without an intermediate `String` per operand (unlike
|
|
1179
|
+
/// `format!("{}{}", a.to_display_string(), b.to_display_string())`, which triple-allocates).
|
|
1180
|
+
fn append_value_for_string_concat(out: &mut String, v: &Value) {
|
|
1181
|
+
use std::fmt::Write;
|
|
1182
|
+
match v {
|
|
1183
|
+
Value::Number(n) => {
|
|
1184
|
+
if n.is_nan() {
|
|
1185
|
+
out.push_str("NaN");
|
|
1186
|
+
} else if *n == f64::INFINITY {
|
|
1187
|
+
out.push_str("Infinity");
|
|
1188
|
+
} else if *n == f64::NEG_INFINITY {
|
|
1189
|
+
out.push_str("-Infinity");
|
|
1190
|
+
} else {
|
|
1191
|
+
let _ = write!(out, "{n}");
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
Value::String(s) => out.push_str(s.as_ref()),
|
|
1195
|
+
Value::Bool(b) => out.push_str(if *b { "true" } else { "false" }),
|
|
1196
|
+
Value::Null => out.push_str("null"),
|
|
1197
|
+
_ => out.push_str(&v.to_display_string()),
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1115
1201
|
fn eval_binop(op: BinOp, l: &Value, r: &Value) -> Result<Value, String> {
|
|
1116
1202
|
use tishlang_ast::BinOp::*;
|
|
1117
1203
|
use tishlang_core::Value::*;
|
|
@@ -1120,7 +1206,11 @@ fn eval_binop(op: BinOp, l: &Value, r: &Value) -> Result<Value, String> {
|
|
|
1120
1206
|
match op {
|
|
1121
1207
|
Add => {
|
|
1122
1208
|
if matches!(l, Value::String(_)) || matches!(r, Value::String(_)) {
|
|
1123
|
-
|
|
1209
|
+
let cap = estimate_string_concat_len(l) + estimate_string_concat_len(r);
|
|
1210
|
+
let mut buf = std::string::String::with_capacity(cap);
|
|
1211
|
+
append_value_for_string_concat(&mut buf, l);
|
|
1212
|
+
append_value_for_string_concat(&mut buf, r);
|
|
1213
|
+
Ok(String(buf.into()))
|
|
1124
1214
|
} else {
|
|
1125
1215
|
Ok(Number(ln + rn))
|
|
1126
1216
|
}
|
|
@@ -1146,9 +1236,13 @@ fn eval_binop(op: BinOp, l: &Value, r: &Value) -> Result<Value, String> {
|
|
|
1146
1236
|
Shl => Ok(Number(((ln as i32) << (rn as i32)) as f64)),
|
|
1147
1237
|
Shr => Ok(Number(((ln as i32) >> (rn as i32)) as f64)),
|
|
1148
1238
|
In => {
|
|
1149
|
-
let key_s: Arc<str> = l
|
|
1239
|
+
let key_s: Arc<str> = match l {
|
|
1240
|
+
Value::String(s) => Arc::clone(s),
|
|
1241
|
+
Value::Number(n) => n.to_string().into(),
|
|
1242
|
+
_ => l.to_display_string().into(),
|
|
1243
|
+
};
|
|
1150
1244
|
Ok(Bool(match r {
|
|
1151
|
-
Value::Object(m) => m.borrow().contains_key(
|
|
1245
|
+
Value::Object(m) => m.borrow().contains_key(key_s.as_ref()),
|
|
1152
1246
|
Value::Array(a) => {
|
|
1153
1247
|
if key_s.as_ref() == "length" {
|
|
1154
1248
|
true
|
package/justfile
CHANGED
|
@@ -125,15 +125,15 @@ test *ARGS:
|
|
|
125
125
|
|
|
126
126
|
# Run only tish package tests (same as CI: integration tests only)
|
|
127
127
|
test-tish *ARGS:
|
|
128
|
-
cargo nextest run -p
|
|
128
|
+
cargo nextest run -p tishlang--features full -- {{ARGS}}
|
|
129
129
|
|
|
130
130
|
# Skip slow backend tests (native/cranelift/wasi) for fast local iteration
|
|
131
131
|
test-quick:
|
|
132
|
-
cargo nextest run -p
|
|
132
|
+
cargo nextest run -p tishlang--features full -- --skip test_mvp_programs_native --skip test_mvp_programs_cranelift --skip test_mvp_programs_wasi
|
|
133
133
|
|
|
134
134
|
# Run tests with coverage (requires llvm-tools: rustup component add llvm-tools-preview)
|
|
135
135
|
test-coverage:
|
|
136
|
-
cargo llvm-cov nextest -p
|
|
136
|
+
cargo llvm-cov nextest -p tishlang--features full --lcov --output-path lcov.info --html coverage-html
|
|
137
137
|
|
|
138
138
|
# Plain cargo test (whole workspace)
|
|
139
139
|
test-cargo:
|
package/package.json
CHANGED
|
Binary file
|
package/platform/darwin-x64/tish
CHANGED
|
Binary file
|
|
Binary file
|
package/platform/linux-x64/tish
CHANGED
|
Binary file
|
|
Binary file
|