@tishlang/tish 1.0.7 → 1.0.10

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 (127) hide show
  1. package/Cargo.toml +43 -0
  2. package/LICENSE +13 -0
  3. package/README.md +66 -0
  4. package/crates/js_to_tish/Cargo.toml +9 -0
  5. package/crates/js_to_tish/README.md +18 -0
  6. package/crates/js_to_tish/src/error.rs +61 -0
  7. package/crates/js_to_tish/src/lib.rs +11 -0
  8. package/crates/js_to_tish/src/span_util.rs +35 -0
  9. package/crates/js_to_tish/src/transform/expr.rs +608 -0
  10. package/crates/js_to_tish/src/transform/stmt.rs +474 -0
  11. package/crates/js_to_tish/src/transform.rs +60 -0
  12. package/crates/tish/Cargo.toml +44 -0
  13. package/crates/tish/src/main.rs +585 -0
  14. package/crates/tish/src/repl_completion.rs +200 -0
  15. package/crates/tish/tests/integration_test.rs +726 -0
  16. package/crates/tish_ast/Cargo.toml +7 -0
  17. package/crates/tish_ast/src/ast.rs +494 -0
  18. package/crates/tish_ast/src/lib.rs +5 -0
  19. package/crates/tish_build_utils/Cargo.toml +5 -0
  20. package/crates/tish_build_utils/src/lib.rs +175 -0
  21. package/crates/tish_builtins/Cargo.toml +12 -0
  22. package/crates/tish_builtins/src/array.rs +410 -0
  23. package/crates/tish_builtins/src/globals.rs +197 -0
  24. package/crates/tish_builtins/src/helpers.rs +38 -0
  25. package/crates/tish_builtins/src/lib.rs +14 -0
  26. package/crates/tish_builtins/src/math.rs +80 -0
  27. package/crates/tish_builtins/src/object.rs +36 -0
  28. package/crates/tish_builtins/src/string.rs +253 -0
  29. package/crates/tish_bytecode/Cargo.toml +15 -0
  30. package/crates/tish_bytecode/src/chunk.rs +97 -0
  31. package/crates/tish_bytecode/src/compiler.rs +1361 -0
  32. package/crates/tish_bytecode/src/encoding.rs +100 -0
  33. package/crates/tish_bytecode/src/lib.rs +19 -0
  34. package/crates/tish_bytecode/src/opcode.rs +110 -0
  35. package/crates/tish_bytecode/src/peephole.rs +159 -0
  36. package/crates/tish_bytecode/src/serialize.rs +163 -0
  37. package/crates/tish_bytecode/tests/constant_folding.rs +84 -0
  38. package/crates/tish_bytecode/tests/shortcircuit.rs +49 -0
  39. package/crates/tish_bytecode/tests/sort_optimization.rs +31 -0
  40. package/crates/tish_compile/Cargo.toml +21 -0
  41. package/crates/tish_compile/src/codegen.rs +3316 -0
  42. package/crates/tish_compile/src/lib.rs +71 -0
  43. package/crates/tish_compile/src/resolve.rs +631 -0
  44. package/crates/tish_compile/src/types.rs +304 -0
  45. package/crates/tish_compile_js/Cargo.toml +16 -0
  46. package/crates/tish_compile_js/examples/jsx_vdom_smoke.tish +8 -0
  47. package/crates/tish_compile_js/src/codegen.rs +794 -0
  48. package/crates/tish_compile_js/src/error.rs +20 -0
  49. package/crates/tish_compile_js/src/js_intrinsics.rs +82 -0
  50. package/crates/tish_compile_js/src/lib.rs +27 -0
  51. package/crates/tish_compile_js/src/tests_jsx.rs +32 -0
  52. package/crates/tish_compiler_wasm/Cargo.toml +19 -0
  53. package/crates/tish_compiler_wasm/src/lib.rs +55 -0
  54. package/crates/tish_compiler_wasm/src/resolve_virtual.rs +462 -0
  55. package/crates/tish_core/Cargo.toml +11 -0
  56. package/crates/tish_core/src/console_style.rs +128 -0
  57. package/crates/tish_core/src/json.rs +327 -0
  58. package/crates/tish_core/src/lib.rs +15 -0
  59. package/crates/tish_core/src/macros.rs +37 -0
  60. package/crates/tish_core/src/uri.rs +115 -0
  61. package/crates/tish_core/src/value.rs +376 -0
  62. package/crates/tish_cranelift/Cargo.toml +17 -0
  63. package/crates/tish_cranelift/src/lib.rs +41 -0
  64. package/crates/tish_cranelift/src/link.rs +120 -0
  65. package/crates/tish_cranelift/src/lower.rs +77 -0
  66. package/crates/tish_cranelift_runtime/Cargo.toml +19 -0
  67. package/crates/tish_cranelift_runtime/src/lib.rs +43 -0
  68. package/crates/tish_eval/Cargo.toml +26 -0
  69. package/crates/tish_eval/src/eval.rs +3205 -0
  70. package/crates/tish_eval/src/http.rs +122 -0
  71. package/crates/tish_eval/src/lib.rs +59 -0
  72. package/crates/tish_eval/src/natives.rs +301 -0
  73. package/crates/tish_eval/src/promise.rs +173 -0
  74. package/crates/tish_eval/src/regex.rs +298 -0
  75. package/crates/tish_eval/src/timers.rs +111 -0
  76. package/crates/tish_eval/src/value.rs +224 -0
  77. package/crates/tish_eval/src/value_convert.rs +85 -0
  78. package/crates/tish_fmt/Cargo.toml +16 -0
  79. package/crates/tish_fmt/src/bin/tish-fmt.rs +41 -0
  80. package/crates/tish_fmt/src/lib.rs +884 -0
  81. package/crates/tish_jsx_web/Cargo.toml +7 -0
  82. package/crates/tish_jsx_web/README.md +18 -0
  83. package/crates/tish_jsx_web/src/lib.rs +157 -0
  84. package/crates/tish_jsx_web/vendor/Lattish.tish +347 -0
  85. package/crates/tish_lexer/Cargo.toml +7 -0
  86. package/crates/tish_lexer/src/lib.rs +430 -0
  87. package/crates/tish_lexer/src/token.rs +155 -0
  88. package/crates/tish_lint/Cargo.toml +17 -0
  89. package/crates/tish_lint/src/bin/tish-lint.rs +77 -0
  90. package/crates/tish_lint/src/lib.rs +278 -0
  91. package/crates/tish_llvm/Cargo.toml +11 -0
  92. package/crates/tish_llvm/src/lib.rs +106 -0
  93. package/crates/tish_lsp/Cargo.toml +22 -0
  94. package/crates/tish_lsp/README.md +26 -0
  95. package/crates/tish_lsp/src/main.rs +615 -0
  96. package/crates/tish_native/Cargo.toml +14 -0
  97. package/crates/tish_native/src/build.rs +102 -0
  98. package/crates/tish_native/src/lib.rs +237 -0
  99. package/crates/tish_opt/Cargo.toml +11 -0
  100. package/crates/tish_opt/src/lib.rs +896 -0
  101. package/crates/tish_parser/Cargo.toml +9 -0
  102. package/crates/tish_parser/src/lib.rs +123 -0
  103. package/crates/tish_parser/src/parser.rs +1714 -0
  104. package/crates/tish_runtime/Cargo.toml +26 -0
  105. package/crates/tish_runtime/src/http.rs +308 -0
  106. package/crates/tish_runtime/src/http_fetch.rs +453 -0
  107. package/crates/tish_runtime/src/lib.rs +1004 -0
  108. package/crates/tish_runtime/src/native_promise.rs +26 -0
  109. package/crates/tish_runtime/src/promise.rs +77 -0
  110. package/crates/tish_runtime/src/promise_io.rs +41 -0
  111. package/crates/tish_runtime/src/timers.rs +125 -0
  112. package/crates/tish_runtime/src/ws.rs +725 -0
  113. package/crates/tish_runtime/tests/fetch_readable_stream.rs +99 -0
  114. package/crates/tish_vm/Cargo.toml +31 -0
  115. package/crates/tish_vm/src/lib.rs +39 -0
  116. package/crates/tish_vm/src/vm.rs +1399 -0
  117. package/crates/tish_wasm/Cargo.toml +13 -0
  118. package/crates/tish_wasm/src/lib.rs +358 -0
  119. package/crates/tish_wasm_runtime/Cargo.toml +25 -0
  120. package/crates/tish_wasm_runtime/src/lib.rs +36 -0
  121. package/justfile +260 -0
  122. package/package.json +8 -3
  123. package/platform/darwin-arm64/tish +0 -0
  124. package/platform/darwin-x64/tish +0 -0
  125. package/platform/linux-arm64/tish +0 -0
  126. package/platform/linux-x64/tish +0 -0
  127. package/platform/win32-x64/tish.exe +0 -0
@@ -0,0 +1,462 @@
1
+ //! Virtual module resolution for browser/playground. No filesystem.
2
+ //! Resolves imports from an in-memory file map, merges modules into a single Program.
3
+
4
+ use std::collections::{HashMap, HashSet};
5
+ use std::sync::Arc;
6
+
7
+ use tish_ast::{ExportDeclaration, Expr, ImportSpecifier, Program, Statement};
8
+
9
+ /// A resolved module: virtual path and its parsed program.
10
+ #[derive(Debug, Clone)]
11
+ pub struct VirtualModule {
12
+ pub path: String,
13
+ pub program: Program,
14
+ }
15
+
16
+ /// Node-compatible aliases for built-in modules (fs -> tish:fs, etc.).
17
+ const BUILTIN_ALIASES: &[(&str, &str)] = &[
18
+ ("fs", "tish:fs"),
19
+ ("http", "tish:http"),
20
+ ("process", "tish:process"),
21
+ ("ws", "tish:ws"),
22
+ ];
23
+
24
+ fn normalize_builtin_spec(spec: &str) -> Option<String> {
25
+ if spec.starts_with("tish:") {
26
+ return Some(spec.to_string());
27
+ }
28
+ BUILTIN_ALIASES
29
+ .iter()
30
+ .find(|(alias, _)| *alias == spec)
31
+ .map(|(_, canonical)| (*canonical).to_string())
32
+ }
33
+
34
+ fn is_native_import(spec: &str) -> bool {
35
+ spec.starts_with("tish:")
36
+ || spec.starts_with('@')
37
+ || matches!(spec, "fs" | "http" | "process" | "ws")
38
+ }
39
+
40
+ /// Normalize a virtual path: resolve . and .. components.
41
+ /// e.g. "sub/../lib.tish" -> "lib.tish", "./foo.tish" -> "foo.tish"
42
+ fn normalize_virtual_path(from_dir: &str, spec: &str) -> Result<String, String> {
43
+ if !spec.starts_with("./") && !spec.starts_with("../") {
44
+ return Err(format!(
45
+ "Only relative imports (./, ../) or native imports (tish:*, @scope/pkg) are supported. Got: {}",
46
+ spec
47
+ ));
48
+ }
49
+ let combined = if from_dir.is_empty() {
50
+ spec.to_string()
51
+ } else {
52
+ format!("{}/{}", from_dir, spec)
53
+ };
54
+ let parts: Vec<&str> = combined.split('/').collect();
55
+ let mut stack: Vec<&str> = Vec::new();
56
+ for p in parts {
57
+ match p {
58
+ "" | "." => {}
59
+ ".." => {
60
+ stack.pop();
61
+ }
62
+ _ => stack.push(p),
63
+ }
64
+ }
65
+ Ok(stack.join("/"))
66
+ }
67
+
68
+ /// Parent directory of a virtual path. "main.tish" -> "", "sub/foo.tish" -> "sub"
69
+ fn parent_dir(path: &str) -> &str {
70
+ if let Some(slash) = path.rfind('/') {
71
+ &path[..slash]
72
+ } else {
73
+ ""
74
+ }
75
+ }
76
+
77
+ /// Resolve import spec to a key for the files map. Tries .tish extension if missing.
78
+ fn resolve_import_to_key(
79
+ spec: &str,
80
+ from_dir: &str,
81
+ files: &HashMap<String, String>,
82
+ ) -> Result<String, String> {
83
+ let normalized = normalize_virtual_path(from_dir, spec)?;
84
+ if files.contains_key(&normalized) {
85
+ return Ok(normalized);
86
+ }
87
+ if !normalized.ends_with(".tish") && !normalized.contains('.') {
88
+ let with_ext = format!("{}.tish", normalized);
89
+ if files.contains_key(&with_ext) {
90
+ return Ok(with_ext);
91
+ }
92
+ }
93
+ Err(format!(
94
+ "Cannot resolve import '{}' from {}: file not in virtual file map",
95
+ spec, from_dir
96
+ ))
97
+ }
98
+
99
+ /// Resolve all modules starting from the entry file. Returns modules in dependency order.
100
+ pub fn resolve_virtual(
101
+ entry_path: &str,
102
+ files: &HashMap<String, String>,
103
+ ) -> Result<Vec<VirtualModule>, String> {
104
+ let entry_key = if files.contains_key(entry_path) {
105
+ entry_path.to_string()
106
+ } else if !entry_path.ends_with(".tish") {
107
+ let with_ext = format!("{}.tish", entry_path);
108
+ if files.contains_key(&with_ext) {
109
+ with_ext
110
+ } else {
111
+ return Err(format!("Entry file '{}' not in virtual file map", entry_path));
112
+ }
113
+ } else {
114
+ return Err(format!("Entry file '{}' not in virtual file map", entry_path));
115
+ };
116
+
117
+ let mut visited = HashSet::new();
118
+ let mut path_to_module: HashMap<String, Program> = HashMap::new();
119
+ let mut load_order: Vec<String> = Vec::new();
120
+
121
+ load_module_recursive(
122
+ &entry_key,
123
+ files,
124
+ &mut visited,
125
+ &mut path_to_module,
126
+ &mut load_order,
127
+ )?;
128
+
129
+ Ok(load_order
130
+ .into_iter()
131
+ .map(|p| {
132
+ let program = path_to_module.remove(&p).unwrap();
133
+ VirtualModule { path: p, program }
134
+ })
135
+ .collect())
136
+ }
137
+
138
+ fn load_module_recursive(
139
+ module_path: &str,
140
+ files: &HashMap<String, String>,
141
+ visited: &mut HashSet<String>,
142
+ path_to_module: &mut HashMap<String, Program>,
143
+ load_order: &mut Vec<String>,
144
+ ) -> Result<(), String> {
145
+ if visited.contains(module_path) {
146
+ return Ok(());
147
+ }
148
+ visited.insert(module_path.to_string());
149
+
150
+ let source = files.get(module_path).ok_or_else(|| {
151
+ format!("Module '{}' not in virtual file map", module_path)
152
+ })?;
153
+ let program = tish_parser::parse(source.trim())
154
+ .map_err(|e| format!("Parse error in {}: {}", module_path, e))?;
155
+
156
+ let from_dir = parent_dir(module_path);
157
+ for stmt in &program.statements {
158
+ if let Statement::Import { from, .. } = stmt {
159
+ if is_native_import(from) {
160
+ continue;
161
+ }
162
+ let dep_key = resolve_import_to_key(from, from_dir, files)?;
163
+ if !path_to_module.contains_key(&dep_key) {
164
+ load_module_recursive(
165
+ &dep_key,
166
+ files,
167
+ visited,
168
+ path_to_module,
169
+ load_order,
170
+ )?;
171
+ }
172
+ }
173
+ }
174
+
175
+ path_to_module.insert(module_path.to_string(), program);
176
+ load_order.push(module_path.to_string());
177
+ Ok(())
178
+ }
179
+
180
+ /// Check for cyclic imports.
181
+ pub fn detect_cycles_virtual(modules: &[VirtualModule]) -> Result<(), String> {
182
+ let path_to_idx: HashMap<_, _> = modules
183
+ .iter()
184
+ .enumerate()
185
+ .map(|(i, m)| (m.path.clone(), i))
186
+ .collect();
187
+
188
+ for (idx, module) in modules.iter().enumerate() {
189
+ let dir = parent_dir(&module.path);
190
+ let mut stack = vec![idx];
191
+ if has_cycle_from(
192
+ dir,
193
+ &module.program,
194
+ &path_to_idx,
195
+ modules,
196
+ &mut stack,
197
+ &mut HashSet::new(),
198
+ )? {
199
+ let path_names: Vec<_> = stack
200
+ .iter()
201
+ .map(|&i| modules[i].path.clone())
202
+ .collect();
203
+ return Err(format!("Circular import detected: {}", path_names.join(" -> ")));
204
+ }
205
+ }
206
+ Ok(())
207
+ }
208
+
209
+ fn has_cycle_from(
210
+ from_dir: &str,
211
+ program: &Program,
212
+ path_to_idx: &HashMap<String, usize>,
213
+ modules: &[VirtualModule],
214
+ stack: &mut Vec<usize>,
215
+ visiting: &mut HashSet<usize>,
216
+ ) -> Result<bool, String> {
217
+ for stmt in &program.statements {
218
+ if let Statement::Import { from, .. } = stmt {
219
+ if is_native_import(from) {
220
+ continue;
221
+ }
222
+ let dep_key = resolve_import_to_key_for_cycle(from, from_dir, path_to_idx)?;
223
+ if let Some(&dep_idx) = path_to_idx.get(&dep_key) {
224
+ if stack.contains(&dep_idx) {
225
+ stack.push(dep_idx);
226
+ return Ok(true);
227
+ }
228
+ if !visiting.contains(&dep_idx) {
229
+ visiting.insert(dep_idx);
230
+ stack.push(dep_idx);
231
+ let dep = &modules[dep_idx];
232
+ let dep_dir = parent_dir(&dep.path);
233
+ if has_cycle_from(
234
+ dep_dir,
235
+ &dep.program,
236
+ path_to_idx,
237
+ modules,
238
+ stack,
239
+ visiting,
240
+ )? {
241
+ return Ok(true);
242
+ }
243
+ stack.pop();
244
+ visiting.remove(&dep_idx);
245
+ }
246
+ }
247
+ }
248
+ }
249
+ Ok(false)
250
+ }
251
+
252
+ fn resolve_import_to_key_for_cycle(
253
+ spec: &str,
254
+ from_dir: &str,
255
+ path_to_idx: &HashMap<String, usize>,
256
+ ) -> Result<String, String> {
257
+ let normalized = normalize_virtual_path(from_dir, spec)?;
258
+ if path_to_idx.contains_key(&normalized) {
259
+ return Ok(normalized);
260
+ }
261
+ if !normalized.ends_with(".tish") && !normalized.contains('.') {
262
+ let with_ext = format!("{}.tish", normalized);
263
+ if path_to_idx.contains_key(&with_ext) {
264
+ return Ok(with_ext);
265
+ }
266
+ }
267
+ Err(format!(
268
+ "Cannot resolve import '{}' from {}: module not in resolved set",
269
+ spec, from_dir
270
+ ))
271
+ }
272
+
273
+ /// Merge all resolved modules into a single program. Dependencies are emitted first.
274
+ pub fn merge_modules_virtual(modules: Vec<VirtualModule>) -> Result<Program, String> {
275
+ let path_to_idx: HashMap<String, usize> = modules
276
+ .iter()
277
+ .enumerate()
278
+ .map(|(i, m)| (m.path.clone(), i))
279
+ .collect();
280
+
281
+ let mut module_exports: Vec<HashMap<String, String>> = vec![HashMap::new(); modules.len()];
282
+ for (idx, module) in modules.iter().enumerate() {
283
+ for stmt in &module.program.statements {
284
+ if let Statement::Export { declaration, .. } = stmt {
285
+ match declaration.as_ref() {
286
+ ExportDeclaration::Named(s) => {
287
+ let name = match s.as_ref() {
288
+ Statement::VarDecl { name, .. } | Statement::FunDecl { name, .. } => {
289
+ name.to_string()
290
+ }
291
+ _ => continue,
292
+ };
293
+ module_exports[idx].insert(name.clone(), name);
294
+ }
295
+ ExportDeclaration::Default(_) => {
296
+ let default_name = format!("__default_{}", idx);
297
+ module_exports[idx].insert("default".to_string(), default_name);
298
+ }
299
+ }
300
+ }
301
+ }
302
+ }
303
+
304
+ let mut statements = Vec::new();
305
+ for (idx, module) in modules.iter().enumerate() {
306
+ let dir = parent_dir(&module.path);
307
+ for stmt in &module.program.statements {
308
+ match stmt {
309
+ Statement::Import { specifiers, from, span } => {
310
+ if is_native_import(from) {
311
+ let canonical_spec =
312
+ normalize_builtin_spec(from).unwrap_or_else(|| from.to_string());
313
+ for spec in specifiers {
314
+ match spec {
315
+ ImportSpecifier::Named { name, alias } => {
316
+ let bind = alias.as_deref().unwrap_or(name.as_ref());
317
+ let init = Expr::NativeModuleLoad {
318
+ spec: Arc::from(canonical_spec.clone()),
319
+ export_name: name.clone(),
320
+ span: *span,
321
+ };
322
+ statements.push(Statement::VarDecl {
323
+ name: Arc::from(bind),
324
+ mutable: false,
325
+ type_ann: None,
326
+ init: Some(init),
327
+ span: *span,
328
+ });
329
+ }
330
+ ImportSpecifier::Namespace(ns) => {
331
+ return Err(format!(
332
+ "Namespace import (* as {}) not supported for native module '{}'",
333
+ ns.as_ref(),
334
+ from.as_ref()
335
+ ));
336
+ }
337
+ ImportSpecifier::Default(bind) => {
338
+ return Err(format!(
339
+ "Default import '{}' not supported for native module '{}'. Use named import.",
340
+ bind.as_ref(),
341
+ from.as_ref()
342
+ ));
343
+ }
344
+ }
345
+ }
346
+ continue;
347
+ }
348
+ let dep_key = resolve_import_to_key_for_cycle(from, dir, &path_to_idx)?;
349
+ let dep_idx = *path_to_idx
350
+ .get(&dep_key)
351
+ .ok_or_else(|| format!("Resolved import '{}' not in module list", from))?;
352
+ let dep_exports = &module_exports[dep_idx];
353
+ for spec in specifiers {
354
+ match spec {
355
+ ImportSpecifier::Named { name, alias } => {
356
+ let source = dep_exports
357
+ .get(name.as_ref())
358
+ .cloned()
359
+ .unwrap_or_else(|| name.to_string());
360
+ let bind = alias.as_deref().unwrap_or(name.as_ref());
361
+ if bind != source {
362
+ statements.push(Statement::VarDecl {
363
+ name: Arc::from(bind),
364
+ mutable: false,
365
+ type_ann: None,
366
+ init: Some(Expr::Ident {
367
+ name: Arc::from(source),
368
+ span: *span,
369
+ }),
370
+ span: *span,
371
+ });
372
+ }
373
+ }
374
+ ImportSpecifier::Namespace(ns) => {
375
+ let mut props = Vec::new();
376
+ for (k, v) in dep_exports {
377
+ props.push(tish_ast::ObjectProp::KeyValue(
378
+ Arc::from(k.clone()),
379
+ Expr::Ident {
380
+ name: Arc::from(v.clone()),
381
+ span: *span,
382
+ },
383
+ ));
384
+ }
385
+ statements.push(Statement::VarDecl {
386
+ name: ns.clone(),
387
+ mutable: false,
388
+ type_ann: None,
389
+ init: Some(Expr::Object {
390
+ props,
391
+ span: *span,
392
+ }),
393
+ span: *span,
394
+ });
395
+ }
396
+ ImportSpecifier::Default(bind) => {
397
+ let source = dep_exports
398
+ .get("default")
399
+ .cloned()
400
+ .ok_or_else(|| {
401
+ format!("Module '{}' has no default export", from)
402
+ })?;
403
+ statements.push(Statement::VarDecl {
404
+ name: bind.clone(),
405
+ mutable: false,
406
+ type_ann: None,
407
+ init: Some(Expr::Ident {
408
+ name: Arc::from(source),
409
+ span: *span,
410
+ }),
411
+ span: *span,
412
+ });
413
+ }
414
+ }
415
+ }
416
+ }
417
+ Statement::Export { declaration, .. } => {
418
+ match declaration.as_ref() {
419
+ ExportDeclaration::Named(s) => statements.push(*s.clone()),
420
+ ExportDeclaration::Default(e) => {
421
+ let default_name = format!("__default_{}", idx);
422
+ statements.push(Statement::VarDecl {
423
+ name: Arc::from(default_name),
424
+ mutable: false,
425
+ type_ann: None,
426
+ init: Some((*e).clone()),
427
+ span: e.span(),
428
+ });
429
+ }
430
+ }
431
+ }
432
+ _ => statements.push(stmt.clone()),
433
+ }
434
+ }
435
+ }
436
+ Ok(Program { statements })
437
+ }
438
+
439
+ #[cfg(test)]
440
+ mod tests {
441
+ use super::*;
442
+
443
+ #[test]
444
+ fn test_resolve_virtual_simple_import() {
445
+ let mut files = HashMap::new();
446
+ files.insert(
447
+ "lib.tish".to_string(),
448
+ "export fn add(a, b) { return a + b }".to_string(),
449
+ );
450
+ files.insert(
451
+ "main.tish".to_string(),
452
+ "import { add } from \"./lib.tish\"\nconsole.log(add(1, 2))".to_string(),
453
+ );
454
+ let modules = resolve_virtual("main.tish", &files).unwrap();
455
+ assert_eq!(modules.len(), 2);
456
+ assert_eq!(modules[0].path, "lib.tish");
457
+ assert_eq!(modules[1].path, "main.tish");
458
+ detect_cycles_virtual(&modules).unwrap();
459
+ let program = merge_modules_virtual(modules).unwrap();
460
+ assert!(!program.statements.is_empty());
461
+ }
462
+ }
@@ -0,0 +1,11 @@
1
+ [package]
2
+ name = "tish_core"
3
+ version = "0.1.0"
4
+ edition = "2021"
5
+
6
+ [features]
7
+ default = []
8
+ regex = ["dep:fancy-regex"]
9
+
10
+ [dependencies]
11
+ fancy-regex = { version = "0.17.0", optional = true }
@@ -0,0 +1,128 @@
1
+ //! Console styling for values (Node/Bun-style colors).
2
+ //!
3
+ //! Use for REPL, console.log, and any terminal output so numbers, strings,
4
+ //! booleans, null, and object structure are easier to scan.
5
+
6
+ use std::io::IsTerminal;
7
+
8
+ use crate::Value;
9
+
10
+ /// ANSI escape codes (standard 4-bit + bright black for dim).
11
+ const RESET: &str = "\x1b[0m";
12
+ /// Number: yellow (Node-style)
13
+ const NUMBER: &str = "\x1b[33m";
14
+ /// String: green
15
+ const STRING: &str = "\x1b[32m";
16
+ /// Boolean: blue
17
+ const BOOLEAN: &str = "\x1b[34m";
18
+ /// Null: dim grey
19
+ const NULL: &str = "\x1b[90m";
20
+ /// Object keys: cyan
21
+ const KEY: &str = "\x1b[36m";
22
+ /// Punctuation (brackets, commas): dim
23
+ const PUNCT: &str = "\x1b[90m";
24
+ /// Function / special (e.g. [Function]): dim
25
+ const SPECIAL: &str = "\x1b[90m";
26
+
27
+ /// Returns whether console output should use colors (stdout is a TTY).
28
+ pub fn use_console_colors() -> bool {
29
+ std::io::stdout().is_terminal()
30
+ }
31
+
32
+ /// Format a single value for console with optional ANSI colors (Node/Bun-style).
33
+ pub fn format_value_styled(value: &Value, colors: bool) -> String {
34
+ if !colors {
35
+ return value.to_display_string();
36
+ }
37
+ format_value_styled_inner(value, colors)
38
+ }
39
+
40
+ fn format_value_styled_inner(value: &Value, colors: bool) -> String {
41
+ match value {
42
+ Value::Number(n) => {
43
+ let s = if n.is_nan() {
44
+ "NaN".to_string()
45
+ } else if *n == f64::INFINITY {
46
+ "Infinity".to_string()
47
+ } else if *n == f64::NEG_INFINITY {
48
+ "-Infinity".to_string()
49
+ } else {
50
+ n.to_string()
51
+ };
52
+ format!("{NUMBER}{s}{RESET}")
53
+ }
54
+ Value::String(s) => {
55
+ let escaped = escape_string_for_display(s);
56
+ format!("{STRING}\"{escaped}\"{RESET}")
57
+ }
58
+ Value::Bool(b) => format!("{BOOLEAN}{b}{RESET}"),
59
+ Value::Null => format!("{NULL}null{RESET}"),
60
+ Value::Array(arr) => {
61
+ let inner: Vec<String> = arr
62
+ .borrow()
63
+ .iter()
64
+ .map(|v| format_value_styled_inner(v, colors))
65
+ .collect();
66
+ let sep = format!("{PUNCT}, {RESET}");
67
+ format!("{PUNCT}[{RESET}{}{PUNCT}]{RESET}", inner.join(&sep))
68
+ }
69
+ Value::Object(obj) => {
70
+ let inner: Vec<String> = obj
71
+ .borrow()
72
+ .iter()
73
+ .map(|(k, v)| {
74
+ format!(
75
+ "{KEY}{}{RESET}{PUNCT}: {RESET}{}",
76
+ k.as_ref(),
77
+ format_value_styled_inner(v, colors)
78
+ )
79
+ })
80
+ .collect();
81
+ let sep = format!("{PUNCT}, {RESET}");
82
+ format!("{PUNCT}{{{RESET} {} {PUNCT}}}{RESET}", inner.join(&sep))
83
+ }
84
+ Value::Function(_) => format!("{SPECIAL}[Function]{RESET}"),
85
+ Value::Promise(_) => format!("{SPECIAL}[object Promise]{RESET}"),
86
+ Value::Opaque(o) => format!("{SPECIAL}[object {}]{RESET}", o.type_name()),
87
+ #[cfg(feature = "regex")]
88
+ Value::RegExp(re) => {
89
+ let re = re.borrow();
90
+ format!(
91
+ "{PUNCT}/{KEY}{}{RESET}{PUNCT}/{}{RESET}",
92
+ re.source,
93
+ re.flags_string()
94
+ )
95
+ }
96
+ }
97
+ }
98
+
99
+ fn escape_string_for_display(s: &str) -> String {
100
+ let mut out = String::with_capacity(s.len());
101
+ for c in s.chars() {
102
+ match c {
103
+ '\\' => out.push_str("\\\\"),
104
+ '\n' => out.push_str("\\n"),
105
+ '\r' => out.push_str("\\r"),
106
+ '\t' => out.push_str("\\t"),
107
+ '"' => out.push_str("\\\""),
108
+ c => out.push(c),
109
+ }
110
+ }
111
+ out
112
+ }
113
+
114
+ /// Format multiple values for console (e.g. console.log(a, b, c)) with optional colors.
115
+ pub fn format_values_for_console(values: &[Value], colors: bool) -> String {
116
+ let mut iter = values.iter();
117
+ match iter.next() {
118
+ None => String::new(),
119
+ Some(first) => {
120
+ let mut result = format_value_styled(first, colors);
121
+ for v in iter {
122
+ result.push(' ');
123
+ result.push_str(&format_value_styled(v, colors));
124
+ }
125
+ result
126
+ }
127
+ }
128
+ }