@tishlang/tish-format 1.0.12 → 2.0.1
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 +51 -0
- package/LICENSE +13 -0
- package/bin/tish-format +0 -0
- package/crates/js_to_tish/Cargo.toml +11 -0
- package/crates/js_to_tish/README.md +18 -0
- package/crates/js_to_tish/src/error.rs +55 -0
- package/crates/js_to_tish/src/lib.rs +11 -0
- package/crates/js_to_tish/src/span_util.rs +35 -0
- package/crates/js_to_tish/src/transform/expr.rs +611 -0
- package/crates/js_to_tish/src/transform/stmt.rs +503 -0
- package/crates/js_to_tish/src/transform.rs +60 -0
- package/crates/tish/Cargo.toml +62 -0
- package/crates/tish/build.rs +21 -0
- package/crates/tish/src/cargo_native_registry.rs +32 -0
- package/crates/tish/src/cli_help.rs +576 -0
- package/crates/tish/src/main.rs +853 -0
- package/crates/tish/src/repl_completion.rs +199 -0
- package/crates/tish/tests/cargo_example_compile.rs +67 -0
- package/crates/tish/tests/error_source_location.rs +36 -0
- package/crates/tish/tests/fixtures/cargo_example_project/Cargo.toml +3 -0
- package/crates/tish/tests/fixtures/cargo_example_project/crates/demo-shim/Cargo.toml +11 -0
- package/crates/tish/tests/fixtures/cargo_example_project/crates/demo-shim/src/lib.rs +12 -0
- package/crates/tish/tests/fixtures/cargo_example_project/package.json +10 -0
- package/crates/tish/tests/fixtures/cargo_example_project/src/main.tish +3 -0
- package/crates/tish/tests/fixtures/runtime_error_location.tish +5 -0
- package/crates/tish/tests/fixtures/trycatch_runtime_errors.tish +15 -0
- package/crates/tish/tests/fixtures/tty_capability.tish +9 -0
- package/crates/tish/tests/integration_test.rs +1406 -0
- package/crates/tish/tests/run_optimize_stdout_parity.rs +50 -0
- package/crates/tish/tests/shortcircuit.rs +65 -0
- package/crates/tish/tests/trycatch_runtime_errors.rs +45 -0
- package/crates/tish/tests/tty_capability.rs +43 -0
- package/crates/tish_ast/Cargo.toml +9 -0
- package/crates/tish_ast/src/ast.rs +649 -0
- package/crates/tish_ast/src/lib.rs +5 -0
- package/crates/tish_build_utils/Cargo.toml +11 -0
- package/crates/tish_build_utils/src/lib.rs +577 -0
- package/crates/tish_builtins/Cargo.toml +22 -0
- package/crates/tish_builtins/src/array.rs +803 -0
- package/crates/tish_builtins/src/collections.rs +481 -0
- package/crates/tish_builtins/src/construct.rs +199 -0
- package/crates/tish_builtins/src/date.rs +538 -0
- package/crates/tish_builtins/src/globals.rs +293 -0
- package/crates/tish_builtins/src/helpers.rs +35 -0
- package/crates/tish_builtins/src/iterator.rs +129 -0
- package/crates/tish_builtins/src/lib.rs +21 -0
- package/crates/tish_builtins/src/math.rs +89 -0
- package/crates/tish_builtins/src/number.rs +96 -0
- package/crates/tish_builtins/src/object.rs +36 -0
- package/crates/tish_builtins/src/string.rs +646 -0
- package/crates/tish_builtins/src/symbol.rs +83 -0
- package/crates/tish_builtins/src/typedarrays.rs +298 -0
- package/crates/tish_bytecode/Cargo.toml +17 -0
- package/crates/tish_bytecode/src/chunk.rs +164 -0
- package/crates/tish_bytecode/src/compiler.rs +2604 -0
- package/crates/tish_bytecode/src/encoding.rs +102 -0
- package/crates/tish_bytecode/src/lib.rs +20 -0
- package/crates/tish_bytecode/src/opcode.rs +185 -0
- package/crates/tish_bytecode/src/peephole.rs +189 -0
- package/crates/tish_bytecode/src/serialize.rs +193 -0
- package/crates/tish_bytecode/tests/break_continue_bytecode.rs +44 -0
- package/crates/tish_bytecode/tests/constant_folding.rs +84 -0
- package/crates/tish_bytecode/tests/sort_optimization.rs +31 -0
- package/crates/tish_compile/Cargo.toml +27 -0
- package/crates/tish_compile/src/check.rs +774 -0
- package/crates/tish_compile/src/codegen.rs +7317 -0
- package/crates/tish_compile/src/infer.rs +1681 -0
- package/crates/tish_compile/src/lib.rs +206 -0
- package/crates/tish_compile/src/resolve.rs +1951 -0
- package/crates/tish_compile/src/types.rs +605 -0
- package/crates/tish_compile_js/Cargo.toml +18 -0
- package/crates/tish_compile_js/examples/jsx_vdom_smoke.tish +8 -0
- package/crates/tish_compile_js/src/codegen.rs +938 -0
- package/crates/tish_compile_js/src/error.rs +20 -0
- package/crates/tish_compile_js/src/lib.rs +26 -0
- package/crates/tish_compile_js/src/tests_jsx.rs +414 -0
- package/crates/tish_compiler_wasm/Cargo.toml +21 -0
- package/crates/tish_compiler_wasm/src/lib.rs +57 -0
- package/crates/tish_compiler_wasm/src/resolve_virtual.rs +473 -0
- package/crates/tish_core/Cargo.toml +32 -0
- package/crates/tish_core/src/console_style.rs +170 -0
- package/crates/tish_core/src/json.rs +430 -0
- package/crates/tish_core/src/lib.rs +20 -0
- package/crates/tish_core/src/macros.rs +36 -0
- package/crates/tish_core/src/shape.rs +85 -0
- package/crates/tish_core/src/uri.rs +118 -0
- package/crates/tish_core/src/value.rs +1350 -0
- package/crates/tish_core/src/vmref.rs +183 -0
- package/crates/tish_cranelift/Cargo.toml +19 -0
- package/crates/tish_cranelift/src/lib.rs +43 -0
- package/crates/tish_cranelift/src/link.rs +130 -0
- package/crates/tish_cranelift/src/lower.rs +85 -0
- package/crates/tish_cranelift_runtime/Cargo.toml +26 -0
- package/crates/tish_cranelift_runtime/src/lib.rs +45 -0
- package/crates/tish_eval/Cargo.toml +51 -0
- package/crates/tish_eval/src/eval.rs +4265 -0
- package/crates/tish_eval/src/http.rs +191 -0
- package/crates/tish_eval/src/lib.rs +99 -0
- package/crates/tish_eval/src/natives.rs +551 -0
- package/crates/tish_eval/src/promise.rs +179 -0
- package/crates/tish_eval/src/regex.rs +299 -0
- package/crates/tish_eval/src/timers.rs +120 -0
- package/crates/tish_eval/src/value.rs +336 -0
- package/crates/tish_eval/src/value_convert.rs +117 -0
- package/crates/tish_ffi/Cargo.toml +26 -0
- package/crates/tish_ffi/src/lib.rs +518 -0
- package/crates/tish_ffi/tests/fixtures/testmod/Cargo.toml +18 -0
- package/crates/tish_ffi/tests/fixtures/testmod/src/lib.rs +46 -0
- package/crates/tish_ffi/tests/loader.rs +65 -0
- package/crates/tish_fmt/Cargo.toml +16 -0
- package/crates/tish_fmt/src/bin/tish-fmt.rs +41 -0
- package/crates/tish_fmt/src/lib.rs +2157 -0
- package/crates/tish_jsx_web/Cargo.toml +9 -0
- package/crates/tish_jsx_web/README.md +5 -0
- package/crates/tish_jsx_web/src/lib.rs +2 -0
- package/crates/tish_lexer/Cargo.toml +9 -0
- package/crates/tish_lexer/src/lib.rs +1104 -0
- package/crates/tish_lexer/src/token.rs +170 -0
- package/crates/tish_lint/Cargo.toml +18 -0
- package/crates/tish_lint/src/bin/tish-lint.rs +195 -0
- package/crates/tish_lint/src/lib.rs +281 -0
- package/crates/tish_llvm/Cargo.toml +13 -0
- package/crates/tish_llvm/src/lib.rs +115 -0
- package/crates/tish_lsp/Cargo.toml +25 -0
- package/crates/tish_lsp/README.md +26 -0
- package/crates/tish_lsp/src/builtin_goto.rs +362 -0
- package/crates/tish_lsp/src/import_goto.rs +564 -0
- package/crates/tish_lsp/src/main.rs +1459 -0
- package/crates/tish_native/Cargo.toml +16 -0
- package/crates/tish_native/src/build.rs +481 -0
- package/crates/tish_native/src/config.rs +48 -0
- package/crates/tish_native/src/lib.rs +416 -0
- package/crates/tish_opt/Cargo.toml +13 -0
- package/crates/tish_opt/src/lib.rs +1046 -0
- package/crates/tish_parser/Cargo.toml +11 -0
- package/crates/tish_parser/src/lib.rs +386 -0
- package/crates/tish_parser/src/parser.rs +2726 -0
- package/crates/tish_pg/Cargo.toml +34 -0
- package/crates/tish_pg/README.md +38 -0
- package/crates/tish_pg/src/error.rs +52 -0
- package/crates/tish_pg/src/lib.rs +955 -0
- package/crates/tish_resolve/Cargo.toml +13 -0
- package/crates/tish_resolve/src/lib.rs +3601 -0
- package/crates/tish_resolve/src/pos.rs +141 -0
- package/crates/tish_runtime/Cargo.toml +100 -0
- package/crates/tish_runtime/src/http.rs +1347 -0
- package/crates/tish_runtime/src/http_fetch.rs +492 -0
- package/crates/tish_runtime/src/http_hyper.rs +441 -0
- package/crates/tish_runtime/src/http_prefork.rs +189 -0
- package/crates/tish_runtime/src/lib.rs +1447 -0
- package/crates/tish_runtime/src/native_promise.rs +15 -0
- package/crates/tish_runtime/src/promise.rs +558 -0
- package/crates/tish_runtime/src/promise_io.rs +38 -0
- package/crates/tish_runtime/src/timers.rs +172 -0
- package/crates/tish_runtime/src/tty.rs +226 -0
- package/crates/tish_runtime/src/ws.rs +778 -0
- package/crates/tish_runtime/tests/fetch_readable_stream.rs +102 -0
- package/crates/tish_ui/Cargo.toml +17 -0
- package/crates/tish_ui/src/jsx.rs +692 -0
- package/crates/tish_ui/src/lib.rs +20 -0
- package/crates/tish_ui/src/runtime/hooks.rs +573 -0
- package/crates/tish_ui/src/runtime/mod.rs +183 -0
- package/crates/tish_vm/Cargo.toml +60 -0
- package/crates/tish_vm/src/jit.rs +1050 -0
- package/crates/tish_vm/src/lib.rs +41 -0
- package/crates/tish_vm/src/vm.rs +3536 -0
- package/crates/tish_vm/tests/concurrent_shared_state.rs +140 -0
- package/crates/tish_vm/tests/fixtures/or_string_cmd.tish +2 -0
- package/crates/tish_vm/tests/lexical_scope_declare.rs +34 -0
- package/crates/tish_vm/tests/peephole_jump_chain_logical_or.rs +150 -0
- package/crates/tish_wasm/Cargo.toml +15 -0
- package/crates/tish_wasm/src/lib.rs +428 -0
- package/crates/tish_wasm_runtime/Cargo.toml +37 -0
- package/crates/tish_wasm_runtime/src/gpu.rs +429 -0
- package/crates/tish_wasm_runtime/src/lib.rs +42 -0
- package/crates/tishlang_cargo_bindgen/Cargo.toml +26 -0
- package/crates/tishlang_cargo_bindgen/src/classify.rs +261 -0
- package/crates/tishlang_cargo_bindgen/src/discover.rs +125 -0
- package/crates/tishlang_cargo_bindgen/src/infer.rs +382 -0
- package/crates/tishlang_cargo_bindgen/src/lib.rs +349 -0
- package/crates/tishlang_cargo_bindgen/src/main.rs +167 -0
- package/crates/tishlang_cargo_bindgen/src/metadata.rs +117 -0
- package/justfile +276 -0
- package/package.json +2 -2
- package/platform/darwin-arm64/tish-fmt +0 -0
- package/platform/darwin-x64/tish-fmt +0 -0
- package/platform/linux-arm64/tish-fmt +0 -0
- package/platform/linux-x64/tish-fmt +0 -0
- package/platform/win32-x64/tish-fmt.exe +0 -0
|
@@ -0,0 +1,605 @@
|
|
|
1
|
+
//! Type system for Tish static typing.
|
|
2
|
+
//!
|
|
3
|
+
//! Maps TypeAnnotation from the AST to concrete Rust types for code generation.
|
|
4
|
+
|
|
5
|
+
use std::collections::HashMap;
|
|
6
|
+
use std::sync::Arc;
|
|
7
|
+
use tishlang_ast::{BinOp, FunParam, TypeAnnotation, TypedParam};
|
|
8
|
+
|
|
9
|
+
/// Concrete Rust type representation for code generation.
|
|
10
|
+
#[derive(Debug, Clone, PartialEq)]
|
|
11
|
+
pub enum RustType {
|
|
12
|
+
/// Dynamic Value type (untyped or complex types)
|
|
13
|
+
Value,
|
|
14
|
+
/// f64 (for number)
|
|
15
|
+
F64,
|
|
16
|
+
/// String (for string)
|
|
17
|
+
String,
|
|
18
|
+
/// bool (for boolean)
|
|
19
|
+
Bool,
|
|
20
|
+
/// () for void/null
|
|
21
|
+
Unit,
|
|
22
|
+
/// Vec<T> for arrays
|
|
23
|
+
Vec(Box<RustType>),
|
|
24
|
+
/// Option<T> for nullable types (T | null)
|
|
25
|
+
Option(Box<RustType>),
|
|
26
|
+
/// Inline object shape — used during inference / annotation lowering
|
|
27
|
+
/// before a `Named` alias has been registered. Once a corresponding
|
|
28
|
+
/// `type Foo = { ... }` declaration is found in the program, occurrences
|
|
29
|
+
/// of this shape can be canonicalised into `RustType::Named("Foo")`.
|
|
30
|
+
Object(Vec<(Arc<str>, RustType)>),
|
|
31
|
+
/// User-defined named type (a struct emitted by the compiler).
|
|
32
|
+
/// The field list is duplicated here so the codegen can emit struct
|
|
33
|
+
/// literals, member access, and Value-conversion glue without going
|
|
34
|
+
/// back to a global registry on every call site.
|
|
35
|
+
Named {
|
|
36
|
+
name: Arc<str>,
|
|
37
|
+
fields: Vec<(Arc<str>, RustType)>,
|
|
38
|
+
},
|
|
39
|
+
/// Fn trait for typed functions
|
|
40
|
+
Function {
|
|
41
|
+
params: Vec<RustType>,
|
|
42
|
+
returns: Box<RustType>,
|
|
43
|
+
},
|
|
44
|
+
/// Tuple `(T0, T1, …)` for `[T0, T1]` tuple types — a native Rust tuple.
|
|
45
|
+
Tuple(Vec<RustType>),
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
impl RustType {
|
|
49
|
+
/// Convert a TypeAnnotation to a RustType (no alias resolution).
|
|
50
|
+
/// Use [`Self::from_annotation_with_aliases`] when a registry is
|
|
51
|
+
/// available so user-defined `type X = { ... }` aliases land as
|
|
52
|
+
/// `RustType::Named` and can drive struct emission.
|
|
53
|
+
pub fn from_annotation(ann: &TypeAnnotation) -> Self {
|
|
54
|
+
Self::from_annotation_with_aliases(ann, &HashMap::new())
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/// Like [`from_annotation`], but consults `aliases` so a `Simple(name)`
|
|
58
|
+
/// reference to a user-declared `type X = { ... }` resolves to a
|
|
59
|
+
/// `RustType::Named { name, fields }` carrying the struct shape.
|
|
60
|
+
pub fn from_annotation_with_aliases(
|
|
61
|
+
ann: &TypeAnnotation,
|
|
62
|
+
aliases: &HashMap<String, RustType>,
|
|
63
|
+
) -> Self {
|
|
64
|
+
match ann {
|
|
65
|
+
TypeAnnotation::Simple(name) => match name.as_ref() {
|
|
66
|
+
"number" => RustType::F64,
|
|
67
|
+
"string" => RustType::String,
|
|
68
|
+
"boolean" | "bool" => RustType::Bool,
|
|
69
|
+
"void" | "undefined" => RustType::Unit,
|
|
70
|
+
"null" => RustType::Unit,
|
|
71
|
+
"any" => RustType::Value,
|
|
72
|
+
other => {
|
|
73
|
+
// User-declared `type X = { ... }`: lift the inline
|
|
74
|
+
// object shape into a `Named` so the codegen can emit
|
|
75
|
+
// a Rust struct and direct field access for it.
|
|
76
|
+
if let Some(t) = aliases.get(other) {
|
|
77
|
+
if let RustType::Object(fields) = t {
|
|
78
|
+
return RustType::Named {
|
|
79
|
+
name: Arc::from(other),
|
|
80
|
+
fields: fields.clone(),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
return t.clone();
|
|
84
|
+
}
|
|
85
|
+
RustType::Value
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
TypeAnnotation::Array(elem) => {
|
|
89
|
+
RustType::Vec(Box::new(Self::from_annotation_with_aliases(elem, aliases)))
|
|
90
|
+
}
|
|
91
|
+
TypeAnnotation::Object(fields) => {
|
|
92
|
+
let typed_fields: Vec<_> = fields
|
|
93
|
+
.iter()
|
|
94
|
+
.map(|(k, v)| (k.clone(), Self::from_annotation_with_aliases(v, aliases)))
|
|
95
|
+
.collect();
|
|
96
|
+
RustType::Object(typed_fields)
|
|
97
|
+
}
|
|
98
|
+
TypeAnnotation::Function { params, returns } => {
|
|
99
|
+
let typed_params: Vec<_> = params
|
|
100
|
+
.iter()
|
|
101
|
+
.map(|p| Self::from_annotation_with_aliases(p, aliases))
|
|
102
|
+
.collect();
|
|
103
|
+
let typed_returns = Box::new(Self::from_annotation_with_aliases(returns, aliases));
|
|
104
|
+
RustType::Function {
|
|
105
|
+
params: typed_params,
|
|
106
|
+
returns: typed_returns,
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
TypeAnnotation::Union(types) => {
|
|
110
|
+
// Check for T | null pattern -> Option<T>
|
|
111
|
+
if types.len() == 2 {
|
|
112
|
+
let has_null = types
|
|
113
|
+
.iter()
|
|
114
|
+
.any(|t| matches!(t, TypeAnnotation::Simple(s) if s.as_ref() == "null"));
|
|
115
|
+
if has_null {
|
|
116
|
+
let non_null = types.iter().find(
|
|
117
|
+
|t| !matches!(t, TypeAnnotation::Simple(s) if s.as_ref() == "null"),
|
|
118
|
+
);
|
|
119
|
+
if let Some(inner) = non_null {
|
|
120
|
+
return RustType::Option(Box::new(Self::from_annotation_with_aliases(
|
|
121
|
+
inner, aliases,
|
|
122
|
+
)));
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
// Other unions fall back to Value
|
|
127
|
+
RustType::Value
|
|
128
|
+
}
|
|
129
|
+
// `[T0, T1]` -> a native Rust tuple `(T0, T1)`.
|
|
130
|
+
TypeAnnotation::Tuple(elems) => RustType::Tuple(
|
|
131
|
+
elems
|
|
132
|
+
.iter()
|
|
133
|
+
.map(|e| Self::from_annotation_with_aliases(e, aliases))
|
|
134
|
+
.collect(),
|
|
135
|
+
),
|
|
136
|
+
// A literal type lowers to its base primitive.
|
|
137
|
+
TypeAnnotation::Literal(lit) => match lit {
|
|
138
|
+
tishlang_ast::TypeLiteral::Str(_) => RustType::String,
|
|
139
|
+
tishlang_ast::TypeLiteral::Num(_) => RustType::F64,
|
|
140
|
+
tishlang_ast::TypeLiteral::Bool(_) => RustType::Bool,
|
|
141
|
+
},
|
|
142
|
+
// Intersection of object shapes (e.g. `interface X extends Y { … }` → `Y & { … }`):
|
|
143
|
+
// merge the fields into one shape. Registered as a `type` alias, this becomes a native
|
|
144
|
+
// struct. Any non-object member → can't merge → fall back to boxed `Value`.
|
|
145
|
+
TypeAnnotation::Intersection(parts) => {
|
|
146
|
+
let mut fields: Vec<(Arc<str>, RustType)> = Vec::new();
|
|
147
|
+
for p in parts {
|
|
148
|
+
match Self::from_annotation_with_aliases(p, aliases) {
|
|
149
|
+
RustType::Object(fs) | RustType::Named { fields: fs, .. } => {
|
|
150
|
+
for (k, v) in fs {
|
|
151
|
+
if !fields.iter().any(|(ek, _)| *ek == k) {
|
|
152
|
+
fields.push((k, v));
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
_ => return RustType::Value,
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
RustType::Object(fields)
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/// Check if this type is a native Rust type (not Value).
|
|
165
|
+
pub fn is_native(&self) -> bool {
|
|
166
|
+
!matches!(self, RustType::Value)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/// Check if this type is numeric (f64).
|
|
170
|
+
pub fn is_numeric(&self) -> bool {
|
|
171
|
+
matches!(self, RustType::F64)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/// Infer the result type of a binary operation given the operand types.
|
|
175
|
+
/// Returns `None` if native code cannot be emitted (fall back to Value path).
|
|
176
|
+
pub fn result_type_of_binop(op: BinOp, lhs: &RustType, rhs: &RustType) -> Option<RustType> {
|
|
177
|
+
if lhs == &RustType::F64 && rhs == &RustType::F64 {
|
|
178
|
+
match op {
|
|
179
|
+
BinOp::Add | BinOp::Sub | BinOp::Mul | BinOp::Div | BinOp::Mod | BinOp::Pow => {
|
|
180
|
+
Some(RustType::F64)
|
|
181
|
+
}
|
|
182
|
+
// Bitwise / shift ops: JS coerces both sides to int32, computes, and
|
|
183
|
+
// returns a Number — so the native result is still F64. Big win for
|
|
184
|
+
// crypto/hashing loops that would otherwise box every `^`/`>>>`.
|
|
185
|
+
BinOp::BitAnd
|
|
186
|
+
| BinOp::BitOr
|
|
187
|
+
| BinOp::BitXor
|
|
188
|
+
| BinOp::Shl
|
|
189
|
+
| BinOp::Shr
|
|
190
|
+
| BinOp::UShr => Some(RustType::F64),
|
|
191
|
+
BinOp::Lt
|
|
192
|
+
| BinOp::Le
|
|
193
|
+
| BinOp::Gt
|
|
194
|
+
| BinOp::Ge
|
|
195
|
+
| BinOp::StrictEq
|
|
196
|
+
| BinOp::StrictNe => Some(RustType::Bool),
|
|
197
|
+
_ => None,
|
|
198
|
+
}
|
|
199
|
+
} else if lhs == &RustType::Bool && rhs == &RustType::Bool {
|
|
200
|
+
match op {
|
|
201
|
+
BinOp::And | BinOp::Or => Some(RustType::Bool),
|
|
202
|
+
BinOp::StrictEq | BinOp::StrictNe => Some(RustType::Bool),
|
|
203
|
+
_ => None,
|
|
204
|
+
}
|
|
205
|
+
} else if lhs == &RustType::String && rhs == &RustType::String {
|
|
206
|
+
// M2: native string concat + value equality. `+` concatenates; `===`/`!==` compare by
|
|
207
|
+
// value (byte-identical to JS and to the boxed `Value::String` path). Relational
|
|
208
|
+
// `< <= > >=` deliberately stay on the boxed path: JS orders strings by UTF-16 code
|
|
209
|
+
// units while Rust `String` orders by UTF-8 bytes — they diverge outside the BMP.
|
|
210
|
+
match op {
|
|
211
|
+
BinOp::Add => Some(RustType::String),
|
|
212
|
+
BinOp::StrictEq | BinOp::StrictNe => Some(RustType::Bool),
|
|
213
|
+
_ => None,
|
|
214
|
+
}
|
|
215
|
+
} else {
|
|
216
|
+
None
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/// Get the Rust type string for code generation.
|
|
221
|
+
pub fn to_rust_type_str(&self) -> String {
|
|
222
|
+
match self {
|
|
223
|
+
RustType::Value => "Value".to_string(),
|
|
224
|
+
RustType::F64 => "f64".to_string(),
|
|
225
|
+
RustType::String => "String".to_string(),
|
|
226
|
+
RustType::Bool => "bool".to_string(),
|
|
227
|
+
RustType::Unit => "()".to_string(),
|
|
228
|
+
RustType::Vec(inner) => format!("Vec<{}>", inner.to_rust_type_str()),
|
|
229
|
+
RustType::Option(inner) => format!("Option<{}>", inner.to_rust_type_str()),
|
|
230
|
+
RustType::Object(_) => {
|
|
231
|
+
// Anonymous inline shapes don't have a Rust struct; fall
|
|
232
|
+
// back to the dynamic Value path.
|
|
233
|
+
"Value".to_string()
|
|
234
|
+
}
|
|
235
|
+
RustType::Named { name, .. } => named_struct_ident(name),
|
|
236
|
+
RustType::Function { params, returns } => {
|
|
237
|
+
let params_str: Vec<_> = params.iter().map(|p| p.to_rust_type_str()).collect();
|
|
238
|
+
format!(
|
|
239
|
+
"Rc<dyn Fn({}) -> {}>",
|
|
240
|
+
params_str.join(", "),
|
|
241
|
+
returns.to_rust_type_str()
|
|
242
|
+
)
|
|
243
|
+
}
|
|
244
|
+
RustType::Tuple(elems) => tuple_text(&elems.iter().map(|e| e.to_rust_type_str()).collect::<Vec<_>>()),
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/// Get the default value for this type.
|
|
249
|
+
pub fn default_value(&self) -> String {
|
|
250
|
+
match self {
|
|
251
|
+
RustType::Value => "Value::Null".to_string(),
|
|
252
|
+
RustType::F64 => "0.0".to_string(),
|
|
253
|
+
RustType::String => "String::new()".to_string(),
|
|
254
|
+
RustType::Bool => "false".to_string(),
|
|
255
|
+
RustType::Unit => "()".to_string(),
|
|
256
|
+
RustType::Vec(_) => "Vec::new()".to_string(),
|
|
257
|
+
RustType::Option(_) => "None".to_string(),
|
|
258
|
+
RustType::Object(_) => "Value::Null".to_string(),
|
|
259
|
+
RustType::Named { fields, .. } => {
|
|
260
|
+
// Build a literal struct with each field at its own default,
|
|
261
|
+
// so unannotated decls of a typed struct still compile.
|
|
262
|
+
let init = fields
|
|
263
|
+
.iter()
|
|
264
|
+
.map(|(k, t)| format!("{}: {}", field_ident(k), t.default_value()))
|
|
265
|
+
.collect::<Vec<_>>()
|
|
266
|
+
.join(", ");
|
|
267
|
+
format!(
|
|
268
|
+
"{} {{ {} }}",
|
|
269
|
+
named_struct_ident(match self {
|
|
270
|
+
RustType::Named { name, .. } => name,
|
|
271
|
+
_ => unreachable!(),
|
|
272
|
+
}),
|
|
273
|
+
init
|
|
274
|
+
)
|
|
275
|
+
}
|
|
276
|
+
RustType::Function { .. } => "Value::Null".to_string(),
|
|
277
|
+
RustType::Tuple(elems) => {
|
|
278
|
+
tuple_text(&elems.iter().map(|e| e.default_value()).collect::<Vec<_>>())
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/// Generate code to convert from Value to this native type.
|
|
284
|
+
pub fn from_value_expr(&self, value_expr: &str) -> String {
|
|
285
|
+
match self {
|
|
286
|
+
RustType::Tuple(elems) => {
|
|
287
|
+
// `Value::Array([..])` -> `(e0, e1, …)`, converting each slot from its `Value`.
|
|
288
|
+
let parts: Vec<String> = elems
|
|
289
|
+
.iter()
|
|
290
|
+
.enumerate()
|
|
291
|
+
.map(|(i, e)| {
|
|
292
|
+
e.from_value_expr(&format!(
|
|
293
|
+
"_t.get({}).cloned().unwrap_or(Value::Null)",
|
|
294
|
+
i
|
|
295
|
+
))
|
|
296
|
+
})
|
|
297
|
+
.collect();
|
|
298
|
+
format!(
|
|
299
|
+
"match &{} {{ Value::Array(_a) => {{ let _t = _a.borrow(); {} }}, _ => panic!(\"expected tuple\") }}",
|
|
300
|
+
value_expr,
|
|
301
|
+
tuple_text(&parts)
|
|
302
|
+
)
|
|
303
|
+
}
|
|
304
|
+
RustType::Value => value_expr.to_string(),
|
|
305
|
+
RustType::F64 => format!(
|
|
306
|
+
"match &{} {{ Value::Number(n) => *n, _ => panic!(\"expected number\") }}",
|
|
307
|
+
value_expr
|
|
308
|
+
),
|
|
309
|
+
RustType::String => format!(
|
|
310
|
+
"match &{} {{ Value::String(s) => s.to_string(), _ => panic!(\"expected string\") }}",
|
|
311
|
+
value_expr
|
|
312
|
+
),
|
|
313
|
+
RustType::Bool => format!(
|
|
314
|
+
"match &{} {{ Value::Bool(b) => *b, _ => panic!(\"expected boolean\") }}",
|
|
315
|
+
value_expr
|
|
316
|
+
),
|
|
317
|
+
RustType::Unit => "()".to_string(),
|
|
318
|
+
RustType::Vec(inner) => {
|
|
319
|
+
let inner_conversion = inner.from_value_expr("v");
|
|
320
|
+
format!(
|
|
321
|
+
"match &{} {{ Value::Array(arr) => arr.borrow().iter().map(|v| {}).collect(), _ => panic!(\"expected array\") }}",
|
|
322
|
+
value_expr, inner_conversion
|
|
323
|
+
)
|
|
324
|
+
}
|
|
325
|
+
RustType::Option(inner) => {
|
|
326
|
+
let inner_conversion = inner.from_value_expr(value_expr);
|
|
327
|
+
format!(
|
|
328
|
+
"match &{} {{ Value::Null => None, _ => Some({}) }}",
|
|
329
|
+
value_expr, inner_conversion
|
|
330
|
+
)
|
|
331
|
+
}
|
|
332
|
+
RustType::Named { name, fields } => {
|
|
333
|
+
// Each field is fetched out of the Value::Object via
|
|
334
|
+
// `get_prop` and converted to its native type. Falls back
|
|
335
|
+
// to the field's `default_value()` if the field is absent
|
|
336
|
+
// (rare — usually these come from JSON or PG).
|
|
337
|
+
let field_assigns = fields
|
|
338
|
+
.iter()
|
|
339
|
+
.map(|(k, ty)| {
|
|
340
|
+
let fetch =
|
|
341
|
+
format!("tishlang_runtime::get_prop(&{}, {:?})", value_expr, k.as_ref());
|
|
342
|
+
format!("{}: {}", field_ident(k), ty.from_value_expr(&fetch))
|
|
343
|
+
})
|
|
344
|
+
.collect::<Vec<_>>()
|
|
345
|
+
.join(", ");
|
|
346
|
+
format!("{} {{ {} }}", named_struct_ident(name), field_assigns)
|
|
347
|
+
}
|
|
348
|
+
_ => value_expr.to_string(), // Fallback
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/// Generate code to convert from this native type to Value.
|
|
353
|
+
pub fn to_value_expr(&self, native_expr: &str) -> String {
|
|
354
|
+
match self {
|
|
355
|
+
RustType::Tuple(elems) => {
|
|
356
|
+
// `(e0, e1, …)` -> `Value::Array([e0.into_value(), …])`.
|
|
357
|
+
let parts: Vec<String> = elems
|
|
358
|
+
.iter()
|
|
359
|
+
.enumerate()
|
|
360
|
+
.map(|(i, e)| e.to_value_expr(&format!("{}.{}", native_expr, i)))
|
|
361
|
+
.collect();
|
|
362
|
+
format!("Value::Array(VmRef::new(vec![{}]))", parts.join(", "))
|
|
363
|
+
}
|
|
364
|
+
RustType::Value => native_expr.to_string(),
|
|
365
|
+
RustType::F64 => format!("Value::Number({})", native_expr),
|
|
366
|
+
RustType::String => format!("Value::String({}.clone().into())", native_expr),
|
|
367
|
+
RustType::Bool => format!("Value::Bool({})", native_expr),
|
|
368
|
+
RustType::Unit => "Value::Null".to_string(),
|
|
369
|
+
RustType::Vec(inner) => {
|
|
370
|
+
// Use iter()/copied()/cloned() to avoid moving the vector.
|
|
371
|
+
let (iter_suffix, val_expr) = match inner.as_ref() {
|
|
372
|
+
RustType::F64 => (".iter().copied()", "Value::Number(v)".to_string()),
|
|
373
|
+
RustType::Bool => (".iter().copied()", "Value::Bool(v)".to_string()),
|
|
374
|
+
_ => (".iter().cloned()", inner.to_value_expr("v")),
|
|
375
|
+
};
|
|
376
|
+
format!(
|
|
377
|
+
"Value::Array(VmRef::new({}{}.map(|v| {}).collect()))",
|
|
378
|
+
native_expr, iter_suffix, val_expr
|
|
379
|
+
)
|
|
380
|
+
}
|
|
381
|
+
RustType::Option(inner) => {
|
|
382
|
+
let inner_to_value = inner.to_value_expr("v");
|
|
383
|
+
format!(
|
|
384
|
+
"match {} {{ Some(v) => {}, None => Value::Null }}",
|
|
385
|
+
native_expr, inner_to_value
|
|
386
|
+
)
|
|
387
|
+
}
|
|
388
|
+
RustType::Named { fields, .. } => {
|
|
389
|
+
// Walk fields, build an ObjectMap, wrap in Value::Object.
|
|
390
|
+
// The boundary is paid only when crossing into untyped
|
|
391
|
+
// Tish (JSON.stringify, calling a Value::Function, etc.);
|
|
392
|
+
// direct Rust-to-Rust paths between two Named values stay
|
|
393
|
+
// as plain struct moves.
|
|
394
|
+
let inserts = fields
|
|
395
|
+
.iter()
|
|
396
|
+
.map(|(k, ty)| {
|
|
397
|
+
let access = format!("{}.{}", native_expr, field_ident(k));
|
|
398
|
+
// A `Value`-typed field (e.g. from a generic struct `Box<T>`) accessed
|
|
399
|
+
// behind `&self` must be cloned — it isn't `Copy` and `to_value_expr(Value)`
|
|
400
|
+
// is identity. Native field types clone/copy inside their own `to_value_expr`.
|
|
401
|
+
let v_expr = if matches!(ty, RustType::Value) {
|
|
402
|
+
format!("{}.clone()", access)
|
|
403
|
+
} else {
|
|
404
|
+
ty.to_value_expr(&access)
|
|
405
|
+
};
|
|
406
|
+
format!(
|
|
407
|
+
"_om.insert(::std::sync::Arc::from({:?}), {});",
|
|
408
|
+
k.as_ref(),
|
|
409
|
+
v_expr
|
|
410
|
+
)
|
|
411
|
+
})
|
|
412
|
+
.collect::<Vec<_>>()
|
|
413
|
+
.join(" ");
|
|
414
|
+
// `Value::object` wraps the `ObjectMap` in an `ObjectData` (what `Value::Object`
|
|
415
|
+
// actually holds) — emitting `Value::Object(VmRef::new(_om))` directly is a type
|
|
416
|
+
// error (`ObjectMap` != `ObjectData`); only surfaced once a whole struct is boxed
|
|
417
|
+
// into a `Value`, e.g. passing it to a function.
|
|
418
|
+
format!(
|
|
419
|
+
"{{ let mut _om = ObjectMap::default(); {} Value::object(_om) }}",
|
|
420
|
+
inserts
|
|
421
|
+
)
|
|
422
|
+
}
|
|
423
|
+
_ => native_expr.to_string(), // Fallback
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/// Map a Tish type-alias name to the Rust struct identifier we emit.
|
|
429
|
+
/// Prefixed so user names can never collide with runtime types like `Value`.
|
|
430
|
+
/// Render a Rust tuple type/value from its parts, using the `(T,)` form for 1-tuples.
|
|
431
|
+
fn tuple_text(parts: &[String]) -> String {
|
|
432
|
+
if parts.len() == 1 {
|
|
433
|
+
format!("({},)", parts[0])
|
|
434
|
+
} else {
|
|
435
|
+
format!("({})", parts.join(", "))
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
pub fn named_struct_ident(tish_name: &str) -> String {
|
|
440
|
+
format!("TishStruct_{}", tish_name)
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/// Map a Tish field name (`randomNumber`) to a valid Rust identifier
|
|
444
|
+
/// (kept identical here — non-snake-case is allowed via
|
|
445
|
+
/// `#[allow(non_snake_case)]` on the struct, so JS-style camelCase keys
|
|
446
|
+
/// stay readable in the generated source).
|
|
447
|
+
pub fn field_ident(tish_name: &str) -> String {
|
|
448
|
+
// Reserve Rust keywords that would otherwise conflict.
|
|
449
|
+
match tish_name {
|
|
450
|
+
"type" | "ref" | "fn" | "match" | "move" | "mod" | "self" | "Self" | "super" | "use"
|
|
451
|
+
| "where" | "loop" | "yield" | "async" | "await" | "dyn" | "impl" | "trait" | "in"
|
|
452
|
+
| "as" | "box" | "crate" | "const" | "extern" | "let" | "mut" | "pub" | "static"
|
|
453
|
+
| "unsafe" | "abstract" | "become" | "do" | "final" | "macro" | "override" | "priv"
|
|
454
|
+
| "typeof" | "unsized" | "virtual" => format!("r#{}", tish_name),
|
|
455
|
+
_ => tish_name.to_string(),
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/// Type context for tracking variable types during code generation.
|
|
460
|
+
#[derive(Debug, Clone, Default)]
|
|
461
|
+
pub struct TypeContext {
|
|
462
|
+
/// Stack of scopes, each mapping variable names to their types
|
|
463
|
+
scopes: Vec<HashMap<String, RustType>>,
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
impl TypeContext {
|
|
467
|
+
pub fn new() -> Self {
|
|
468
|
+
Self {
|
|
469
|
+
scopes: vec![HashMap::new()], // Start with global scope
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/// Enter a new scope (e.g., function body, block).
|
|
474
|
+
pub fn push_scope(&mut self) {
|
|
475
|
+
self.scopes.push(HashMap::new());
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/// Exit the current scope.
|
|
479
|
+
pub fn pop_scope(&mut self) {
|
|
480
|
+
self.scopes.pop();
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/// Push a scope for a function or arrow body and record formals as [`RustType::Value`].
|
|
484
|
+
///
|
|
485
|
+
/// Native codegen always binds parameters from `args.get(i)` as `Value`; this prevents
|
|
486
|
+
/// outer locals (e.g. a loop counter inferred as [`RustType::F64`]) from shadowing the
|
|
487
|
+
/// wrong type for the same identifier.
|
|
488
|
+
pub fn push_fun_param_scope(&mut self, params: &[FunParam], rest_param: Option<&TypedParam>) {
|
|
489
|
+
self.push_scope();
|
|
490
|
+
for p in params {
|
|
491
|
+
for name in p.bound_names() {
|
|
492
|
+
self.define(name.as_ref(), RustType::Value);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
if let Some(rp) = rest_param {
|
|
496
|
+
self.define(rp.name.as_ref(), RustType::Value);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/// Define a variable in the current scope.
|
|
501
|
+
pub fn define(&mut self, name: &str, ty: RustType) {
|
|
502
|
+
if let Some(scope) = self.scopes.last_mut() {
|
|
503
|
+
scope.insert(name.to_string(), ty);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/// Look up a variable's type (searches from innermost to outermost scope).
|
|
508
|
+
pub fn lookup(&self, name: &str) -> Option<&RustType> {
|
|
509
|
+
for scope in self.scopes.iter().rev() {
|
|
510
|
+
if let Some(ty) = scope.get(name) {
|
|
511
|
+
return Some(ty);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
None
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/// Check if a variable is typed (has a non-Value type).
|
|
518
|
+
pub fn is_typed(&self, name: &str) -> bool {
|
|
519
|
+
self.lookup(name).map(|ty| ty.is_native()).unwrap_or(false)
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/// Get the type of a variable, defaulting to Value if not found.
|
|
523
|
+
pub fn get_type(&self, name: &str) -> RustType {
|
|
524
|
+
self.lookup(name).cloned().unwrap_or(RustType::Value)
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
#[cfg(test)]
|
|
529
|
+
mod tests {
|
|
530
|
+
use super::*;
|
|
531
|
+
|
|
532
|
+
#[test]
|
|
533
|
+
fn test_simple_types() {
|
|
534
|
+
assert_eq!(
|
|
535
|
+
RustType::from_annotation(&TypeAnnotation::Simple("number".into())),
|
|
536
|
+
RustType::F64
|
|
537
|
+
);
|
|
538
|
+
assert_eq!(
|
|
539
|
+
RustType::from_annotation(&TypeAnnotation::Simple("string".into())),
|
|
540
|
+
RustType::String
|
|
541
|
+
);
|
|
542
|
+
assert_eq!(
|
|
543
|
+
RustType::from_annotation(&TypeAnnotation::Simple("boolean".into())),
|
|
544
|
+
RustType::Bool
|
|
545
|
+
);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
#[test]
|
|
549
|
+
fn test_array_type() {
|
|
550
|
+
let arr_type = TypeAnnotation::Array(Box::new(TypeAnnotation::Simple("number".into())));
|
|
551
|
+
assert_eq!(
|
|
552
|
+
RustType::from_annotation(&arr_type),
|
|
553
|
+
RustType::Vec(Box::new(RustType::F64))
|
|
554
|
+
);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
#[test]
|
|
558
|
+
fn test_nullable_type() {
|
|
559
|
+
let nullable = TypeAnnotation::Union(vec![
|
|
560
|
+
TypeAnnotation::Simple("string".into()),
|
|
561
|
+
TypeAnnotation::Simple("null".into()),
|
|
562
|
+
]);
|
|
563
|
+
assert_eq!(
|
|
564
|
+
RustType::from_annotation(&nullable),
|
|
565
|
+
RustType::Option(Box::new(RustType::String))
|
|
566
|
+
);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
#[test]
|
|
570
|
+
fn test_type_context() {
|
|
571
|
+
let mut ctx = TypeContext::new();
|
|
572
|
+
ctx.define("x", RustType::F64);
|
|
573
|
+
assert_eq!(ctx.get_type("x"), RustType::F64);
|
|
574
|
+
assert!(ctx.is_typed("x"));
|
|
575
|
+
|
|
576
|
+
ctx.push_scope();
|
|
577
|
+
ctx.define("y", RustType::String);
|
|
578
|
+
assert_eq!(ctx.get_type("y"), RustType::String);
|
|
579
|
+
assert_eq!(ctx.get_type("x"), RustType::F64); // Can still see outer scope
|
|
580
|
+
|
|
581
|
+
ctx.pop_scope();
|
|
582
|
+
assert_eq!(ctx.get_type("y"), RustType::Value); // y no longer visible
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
#[test]
|
|
586
|
+
fn push_fun_param_scope_shadows_outer() {
|
|
587
|
+
use tishlang_ast::{FunParam, Span, TypedParam};
|
|
588
|
+
|
|
589
|
+
let mut ctx = TypeContext::new();
|
|
590
|
+
ctx.define("n", RustType::F64);
|
|
591
|
+
let params = vec![FunParam::Simple(TypedParam {
|
|
592
|
+
name: "n".into(),
|
|
593
|
+
name_span: Span {
|
|
594
|
+
start: (0, 0),
|
|
595
|
+
end: (0, 0),
|
|
596
|
+
},
|
|
597
|
+
type_ann: None,
|
|
598
|
+
default: None,
|
|
599
|
+
})];
|
|
600
|
+
ctx.push_fun_param_scope(¶ms, None);
|
|
601
|
+
assert_eq!(ctx.get_type("n"), RustType::Value);
|
|
602
|
+
ctx.pop_scope();
|
|
603
|
+
assert_eq!(ctx.get_type("n"), RustType::F64);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "tishlang_compile_js"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
edition = "2021"
|
|
5
|
+
description = "Tish to JavaScript transpiler backend"
|
|
6
|
+
|
|
7
|
+
license-file = { workspace = true }
|
|
8
|
+
repository = { workspace = true }
|
|
9
|
+
[features]
|
|
10
|
+
default = []
|
|
11
|
+
|
|
12
|
+
[dependencies]
|
|
13
|
+
sourcemap = "9.3"
|
|
14
|
+
tishlang_ast = { path = "../tish_ast", version = ">=0.1" }
|
|
15
|
+
tishlang_compile = { path = "../tish_compile", version = ">=0.1" }
|
|
16
|
+
tishlang_opt = { path = "../tish_opt", version = ">=0.1" }
|
|
17
|
+
tishlang_parser = { path = "../tish_parser", version = ">=0.1" }
|
|
18
|
+
tishlang_ui = { path = "../tish_ui", version = ">=0.1", default-features = false, features = ["compiler"] }
|