@tishlang/tish-format 1.0.13 → 2.0.2
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 +2 -0
- package/bin/tish-format +0 -0
- package/crates/js_to_tish/src/transform/expr.rs +1 -0
- package/crates/tish/Cargo.toml +10 -2
- package/crates/tish/build.rs +21 -0
- package/crates/tish/src/cli_help.rs +15 -4
- package/crates/tish/src/main.rs +93 -21
- package/crates/tish/src/repl_completion.rs +0 -1
- package/crates/tish/tests/error_source_location.rs +36 -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 +402 -91
- package/crates/tish/tests/trycatch_runtime_errors.rs +45 -0
- package/crates/tish/tests/tty_capability.rs +43 -0
- package/crates/tish_ast/src/ast.rs +37 -8
- package/crates/tish_builtins/Cargo.toml +2 -0
- package/crates/tish_builtins/src/array.rs +375 -13
- package/crates/tish_builtins/src/collections.rs +481 -0
- package/crates/tish_builtins/src/construct.rs +59 -19
- package/crates/tish_builtins/src/date.rs +538 -0
- package/crates/tish_builtins/src/globals.rs +86 -6
- package/crates/tish_builtins/src/iterator.rs +129 -0
- package/crates/tish_builtins/src/lib.rs +5 -0
- package/crates/tish_builtins/src/number.rs +96 -0
- package/crates/tish_builtins/src/object.rs +2 -2
- package/crates/tish_builtins/src/string.rs +19 -20
- package/crates/tish_builtins/src/symbol.rs +1 -1
- package/crates/tish_builtins/src/typedarrays.rs +298 -0
- package/crates/tish_bytecode/src/chunk.rs +69 -1
- package/crates/tish_bytecode/src/compiler.rs +933 -89
- package/crates/tish_bytecode/src/encoding.rs +2 -0
- package/crates/tish_bytecode/src/lib.rs +2 -1
- package/crates/tish_bytecode/src/opcode.rs +47 -4
- package/crates/tish_bytecode/src/serialize.rs +31 -1
- package/crates/tish_compile/Cargo.toml +1 -0
- package/crates/tish_compile/src/check.rs +774 -0
- package/crates/tish_compile/src/codegen.rs +2334 -349
- package/crates/tish_compile/src/infer.rs +1395 -6
- package/crates/tish_compile/src/lib.rs +50 -8
- package/crates/tish_compile/src/resolve.rs +584 -21
- package/crates/tish_compile/src/types.rs +106 -2
- package/crates/tish_compile_js/src/codegen.rs +67 -0
- package/crates/tish_compile_js/src/tests_jsx.rs +64 -0
- package/crates/tish_core/Cargo.toml +7 -1
- package/crates/tish_core/src/console_style.rs +11 -1
- package/crates/tish_core/src/json.rs +81 -38
- package/crates/tish_core/src/lib.rs +3 -0
- package/crates/tish_core/src/shape.rs +85 -0
- package/crates/tish_core/src/value.rs +679 -25
- package/crates/tish_core/src/vmref.rs +13 -8
- package/crates/tish_cranelift/src/link.rs +17 -4
- package/crates/tish_cranelift_runtime/Cargo.toml +1 -0
- package/crates/tish_eval/Cargo.toml +6 -0
- package/crates/tish_eval/src/eval.rs +665 -117
- package/crates/tish_eval/src/http.rs +4 -1
- package/crates/tish_eval/src/natives.rs +165 -13
- package/crates/tish_eval/src/value.rs +31 -13
- package/crates/tish_eval/src/value_convert.rs +10 -4
- 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 +1 -1
- package/crates/tish_fmt/src/lib.rs +61 -5
- package/crates/tish_lexer/src/lib.rs +397 -9
- package/crates/tish_lexer/src/token.rs +7 -0
- package/crates/tish_lint/src/lib.rs +2 -10
- package/crates/tish_lsp/src/import_goto.rs +2 -0
- package/crates/tish_lsp/src/main.rs +439 -26
- package/crates/tish_native/src/build.rs +55 -1
- package/crates/tish_opt/src/lib.rs +126 -23
- package/crates/tish_parser/src/lib.rs +55 -1
- package/crates/tish_parser/src/parser.rs +456 -34
- package/crates/tish_pg/src/lib.rs +3 -3
- package/crates/tish_resolve/src/lib.rs +99 -59
- package/crates/tish_runtime/Cargo.toml +4 -0
- package/crates/tish_runtime/src/http.rs +66 -17
- package/crates/tish_runtime/src/http_fetch.rs +29 -8
- package/crates/tish_runtime/src/http_hyper.rs +25 -2
- package/crates/tish_runtime/src/lib.rs +299 -44
- package/crates/tish_runtime/src/promise.rs +328 -18
- package/crates/tish_runtime/src/timers.rs +13 -7
- package/crates/tish_runtime/src/tty.rs +226 -0
- package/crates/tish_runtime/src/ws.rs +35 -18
- package/crates/tish_runtime/tests/fetch_readable_stream.rs +2 -2
- package/crates/tish_ui/src/jsx.rs +10 -0
- package/crates/tish_ui/src/runtime/hooks.rs +19 -15
- package/crates/tish_ui/src/runtime/mod.rs +15 -12
- package/crates/tish_vm/Cargo.toml +14 -1
- package/crates/tish_vm/src/jit.rs +1050 -0
- package/crates/tish_vm/src/lib.rs +2 -0
- package/crates/tish_vm/src/vm.rs +1546 -202
- package/crates/tish_vm/tests/concurrent_shared_state.rs +140 -0
- package/crates/tish_wasm/src/lib.rs +6 -2
- package/crates/tish_wasm_runtime/src/gpu.rs +17 -1
- package/crates/tishlang_cargo_bindgen/src/classify.rs +1 -3
- package/crates/tishlang_cargo_bindgen/src/lib.rs +2 -2
- package/crates/tishlang_cargo_bindgen/src/metadata.rs +1 -1
- package/justfile +8 -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
- package/README.md +0 -138
|
@@ -0,0 +1,774 @@
|
|
|
1
|
+
//! Phase 2: a gradual type checker over `TypeAnnotation`.
|
|
2
|
+
//!
|
|
3
|
+
//! Produces [`TypeDiagnostic`]s for annotation violations that are *provable* from local
|
|
4
|
+
//! information: a `let x: T = e` whose `e` has a concrete conflicting type, a `return e` that
|
|
5
|
+
//! conflicts with the declared return type, an assignment that conflicts with a variable's declared
|
|
6
|
+
//! type, and a call whose argument conflicts with the parameter type.
|
|
7
|
+
//!
|
|
8
|
+
//! It is deliberately **gradual**: when an expression's type can't be determined (a call to a
|
|
9
|
+
//! function with no signature, a dynamic value, `any`), [`synth`] yields `None` and nothing is
|
|
10
|
+
//! flagged — so valid code is never a false positive. `assignable` is likewise conservative: it
|
|
11
|
+
//! only reports a mismatch between two *concretely known* types (`number`/`string`/`boolean`,
|
|
12
|
+
//! arrays of those, and object shapes); anything it can't resolve is treated as compatible.
|
|
13
|
+
//!
|
|
14
|
+
//! This is the soundness/hardening foundation; richer unification, real unions, and control-flow
|
|
15
|
+
//! narrowing come later (and would move the representation to a dedicated `Ty` IR).
|
|
16
|
+
|
|
17
|
+
use std::collections::HashMap;
|
|
18
|
+
use tishlang_ast::{
|
|
19
|
+
ArrayElement, BinOp, CallArg, Expr, FunParam, Literal, MemberProp, ObjectProp, Program, Span,
|
|
20
|
+
Statement, TypeAnnotation, TypeLiteral, UnaryOp,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/// The base primitive of a literal type (`"a"` → `string`, `42` → `number`, `true` → `boolean`).
|
|
24
|
+
fn lit_base(lit: &TypeLiteral) -> TypeAnnotation {
|
|
25
|
+
TypeAnnotation::Simple(
|
|
26
|
+
match lit {
|
|
27
|
+
TypeLiteral::Str(_) => "string",
|
|
28
|
+
TypeLiteral::Num(_) => "number",
|
|
29
|
+
TypeLiteral::Bool(_) => "boolean",
|
|
30
|
+
}
|
|
31
|
+
.into(),
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
#[derive(Debug, Clone)]
|
|
36
|
+
pub struct TypeDiagnostic {
|
|
37
|
+
pub message: String,
|
|
38
|
+
pub span: Span,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
#[derive(Clone)]
|
|
42
|
+
struct FnSig {
|
|
43
|
+
params: Vec<Option<TypeAnnotation>>,
|
|
44
|
+
ret: Option<TypeAnnotation>,
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
struct CheckCtx {
|
|
48
|
+
scopes: Vec<HashMap<String, TypeAnnotation>>,
|
|
49
|
+
sigs: HashMap<String, FnSig>,
|
|
50
|
+
aliases: HashMap<String, TypeAnnotation>,
|
|
51
|
+
ret_stack: Vec<Option<TypeAnnotation>>,
|
|
52
|
+
diags: Vec<TypeDiagnostic>,
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/// Check a program, returning a diagnostic for every provable annotation violation.
|
|
56
|
+
pub fn check_program(program: &Program) -> Vec<TypeDiagnostic> {
|
|
57
|
+
let mut ctx = CheckCtx {
|
|
58
|
+
scopes: vec![HashMap::new()],
|
|
59
|
+
sigs: HashMap::new(),
|
|
60
|
+
aliases: HashMap::new(),
|
|
61
|
+
ret_stack: Vec::new(),
|
|
62
|
+
diags: Vec::new(),
|
|
63
|
+
};
|
|
64
|
+
ctx.collect_aliases(&program.statements);
|
|
65
|
+
ctx.collect_sigs(&program.statements);
|
|
66
|
+
ctx.check_block(&program.statements);
|
|
67
|
+
ctx.diags
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ── helpers: type constructors / predicates ─────────────────────────────────────────────────
|
|
71
|
+
|
|
72
|
+
fn simple(s: &str) -> TypeAnnotation {
|
|
73
|
+
TypeAnnotation::Simple(s.into())
|
|
74
|
+
}
|
|
75
|
+
fn is_any(ann: &TypeAnnotation) -> bool {
|
|
76
|
+
matches!(ann, TypeAnnotation::Simple(s) if s.as_ref() == "any")
|
|
77
|
+
}
|
|
78
|
+
fn is_named(ann: &TypeAnnotation, n: &str) -> bool {
|
|
79
|
+
matches!(ann, TypeAnnotation::Simple(s) if s.as_ref() == n)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/// Display a type for diagnostics (close to the source syntax).
|
|
83
|
+
fn show(ann: &TypeAnnotation) -> String {
|
|
84
|
+
match ann {
|
|
85
|
+
TypeAnnotation::Simple(s) => s.to_string(),
|
|
86
|
+
TypeAnnotation::Array(t) => format!("{}[]", show(t)),
|
|
87
|
+
TypeAnnotation::Object(fs) => {
|
|
88
|
+
let inner: Vec<String> = fs.iter().map(|(k, t)| format!("{}: {}", k, show(t))).collect();
|
|
89
|
+
format!("{{ {} }}", inner.join(", "))
|
|
90
|
+
}
|
|
91
|
+
TypeAnnotation::Function { params, returns } => {
|
|
92
|
+
let ps: Vec<String> = params.iter().map(show).collect();
|
|
93
|
+
format!("({}) => {}", ps.join(", "), show(returns))
|
|
94
|
+
}
|
|
95
|
+
TypeAnnotation::Union(ts) => ts.iter().map(show).collect::<Vec<_>>().join(" | "),
|
|
96
|
+
TypeAnnotation::Tuple(ts) => {
|
|
97
|
+
format!("[{}]", ts.iter().map(show).collect::<Vec<_>>().join(", "))
|
|
98
|
+
}
|
|
99
|
+
TypeAnnotation::Literal(lit) => match lit {
|
|
100
|
+
TypeLiteral::Str(s) => format!("{:?}", s.as_ref()),
|
|
101
|
+
TypeLiteral::Num(n) => format!("{}", n),
|
|
102
|
+
TypeLiteral::Bool(b) => format!("{}", b),
|
|
103
|
+
},
|
|
104
|
+
TypeAnnotation::Intersection(ts) => {
|
|
105
|
+
ts.iter().map(show).collect::<Vec<_>>().join(" & ")
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/// Resolve a `Simple` alias name to its definition (bounded to avoid cycles).
|
|
111
|
+
fn resolve<'a>(
|
|
112
|
+
ann: &'a TypeAnnotation,
|
|
113
|
+
aliases: &'a HashMap<String, TypeAnnotation>,
|
|
114
|
+
depth: u8,
|
|
115
|
+
) -> &'a TypeAnnotation {
|
|
116
|
+
if depth > 8 {
|
|
117
|
+
return ann;
|
|
118
|
+
}
|
|
119
|
+
if let TypeAnnotation::Simple(s) = ann {
|
|
120
|
+
if let Some(t) = aliases.get(s.as_ref()) {
|
|
121
|
+
return resolve(t, aliases, depth + 1);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
ann
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/// Is `actual` assignable to `expected`? Conservative: only the concretely-known primitive / array
|
|
128
|
+
/// / object-shape mismatches return `false`; everything uncertain returns `true` (no false flag).
|
|
129
|
+
fn assignable(
|
|
130
|
+
actual: &TypeAnnotation,
|
|
131
|
+
expected: &TypeAnnotation,
|
|
132
|
+
aliases: &HashMap<String, TypeAnnotation>,
|
|
133
|
+
) -> bool {
|
|
134
|
+
let a = resolve(actual, aliases, 0);
|
|
135
|
+
let e = resolve(expected, aliases, 0);
|
|
136
|
+
if is_any(a) || is_any(e) {
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
// `null`/`void`/`undefined` are leniently compatible (tish uses `null` for optionals; checking
|
|
140
|
+
// it strictly would false-positive without real union/optional support).
|
|
141
|
+
if matches!(a, TypeAnnotation::Simple(s) if matches!(s.as_ref(), "null" | "void" | "undefined")) {
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
use TypeAnnotation::*;
|
|
145
|
+
match (a, e) {
|
|
146
|
+
(Simple(x), Simple(y)) => {
|
|
147
|
+
// Strict only among the three scalar primitives; any user-defined / unresolved name is
|
|
148
|
+
// treated as compatible.
|
|
149
|
+
let strict = |s: &str| matches!(s, "number" | "string" | "boolean");
|
|
150
|
+
if strict(x.as_ref()) && strict(y.as_ref()) {
|
|
151
|
+
x.as_ref() == y.as_ref()
|
|
152
|
+
} else {
|
|
153
|
+
true
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
(Array(ax), Array(ey)) => assignable(ax, ey, aliases),
|
|
157
|
+
// array vs non-array (after alias/any resolution) is a clear mismatch
|
|
158
|
+
(Array(_), Simple(_)) | (Simple(_), Array(_)) => false,
|
|
159
|
+
(Object(af), Object(ef)) => ef.iter().all(|(k, et)| {
|
|
160
|
+
af.iter()
|
|
161
|
+
.find(|(ak, _)| ak.as_ref() == k.as_ref())
|
|
162
|
+
.map(|(_, at)| assignable(at, et, aliases))
|
|
163
|
+
.unwrap_or(false)
|
|
164
|
+
}),
|
|
165
|
+
// a union actual fits only if every member fits; a union expected accepts any matching member
|
|
166
|
+
(Union(axs), _) => axs.iter().all(|t| assignable(t, e, aliases)),
|
|
167
|
+
(_, Union(eys)) => eys.iter().any(|t| assignable(a, t, aliases)),
|
|
168
|
+
// Tuples: same arity, element-wise assignable.
|
|
169
|
+
(Tuple(at), Tuple(et)) => {
|
|
170
|
+
at.len() == et.len() && at.iter().zip(et).all(|(x, y)| assignable(x, y, aliases))
|
|
171
|
+
}
|
|
172
|
+
// A literal type behaves as its base primitive for assignability (gradual — no exact-value
|
|
173
|
+
// enforcement yet), so `"a"` checks like `string`, `42` like `number`.
|
|
174
|
+
(Literal(la), _) => assignable(&lit_base(la), e, aliases),
|
|
175
|
+
(_, Literal(le)) => assignable(a, &lit_base(le), aliases),
|
|
176
|
+
// To satisfy `A & B`, the actual must be assignable to every member.
|
|
177
|
+
(_, Intersection(es)) => es.iter().all(|t| assignable(a, t, aliases)),
|
|
178
|
+
// An intersection actual satisfies `e` if any of its members does.
|
|
179
|
+
(Intersection(as_), _) => as_.iter().any(|t| assignable(t, e, aliases)),
|
|
180
|
+
// anything else (functions, object-vs-named we couldn't resolve, …) -> lenient
|
|
181
|
+
_ => true,
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ── pre-passes: collect aliases + function signatures (recursively) ──────────────────────────
|
|
186
|
+
|
|
187
|
+
impl CheckCtx {
|
|
188
|
+
fn collect_aliases(&mut self, stmts: &[Statement]) {
|
|
189
|
+
for s in stmts {
|
|
190
|
+
match s {
|
|
191
|
+
Statement::TypeAlias { name, ty, .. } => {
|
|
192
|
+
self.aliases.insert(name.to_string(), ty.clone());
|
|
193
|
+
}
|
|
194
|
+
_ => for_each_child_block(s, &mut |b| self.collect_aliases(b)),
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
fn collect_sigs(&mut self, stmts: &[Statement]) {
|
|
200
|
+
for s in stmts {
|
|
201
|
+
if let Statement::FunDecl {
|
|
202
|
+
name,
|
|
203
|
+
params,
|
|
204
|
+
return_type,
|
|
205
|
+
body,
|
|
206
|
+
..
|
|
207
|
+
} = s
|
|
208
|
+
{
|
|
209
|
+
let p = params
|
|
210
|
+
.iter()
|
|
211
|
+
.map(|fp| match fp {
|
|
212
|
+
FunParam::Simple(tp) => tp.type_ann.clone(),
|
|
213
|
+
_ => None,
|
|
214
|
+
})
|
|
215
|
+
.collect();
|
|
216
|
+
self.sigs.insert(
|
|
217
|
+
name.to_string(),
|
|
218
|
+
FnSig {
|
|
219
|
+
params: p,
|
|
220
|
+
ret: return_type.clone(),
|
|
221
|
+
},
|
|
222
|
+
);
|
|
223
|
+
self.collect_sigs(std::slice::from_ref(body));
|
|
224
|
+
} else {
|
|
225
|
+
for_each_child_block(s, &mut |b| self.collect_sigs(b));
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// ── scope helpers ────────────────────────────────────────────────────────────────────────
|
|
231
|
+
|
|
232
|
+
fn push(&mut self) {
|
|
233
|
+
self.scopes.push(HashMap::new());
|
|
234
|
+
}
|
|
235
|
+
fn pop(&mut self) {
|
|
236
|
+
self.scopes.pop();
|
|
237
|
+
}
|
|
238
|
+
fn define(&mut self, name: &str, ty: TypeAnnotation) {
|
|
239
|
+
if let Some(s) = self.scopes.last_mut() {
|
|
240
|
+
s.insert(name.to_string(), ty);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
fn lookup(&self, name: &str) -> Option<TypeAnnotation> {
|
|
244
|
+
self.scopes.iter().rev().find_map(|s| s.get(name).cloned())
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// ── statement checking ─────────────────────────────────────────────────────────────────────
|
|
248
|
+
|
|
249
|
+
fn check_block(&mut self, stmts: &[Statement]) {
|
|
250
|
+
self.push();
|
|
251
|
+
for s in stmts {
|
|
252
|
+
self.check_stmt(s);
|
|
253
|
+
}
|
|
254
|
+
self.pop();
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
fn check_stmt(&mut self, s: &Statement) {
|
|
258
|
+
match s {
|
|
259
|
+
Statement::VarDecl {
|
|
260
|
+
name,
|
|
261
|
+
type_ann,
|
|
262
|
+
init,
|
|
263
|
+
..
|
|
264
|
+
} => {
|
|
265
|
+
if let Some(e) = init {
|
|
266
|
+
let t = self.synth(e);
|
|
267
|
+
if let (Some(ann), Some(t)) = (type_ann, &t) {
|
|
268
|
+
if !assignable(t, ann, &self.aliases) {
|
|
269
|
+
self.diags.push(TypeDiagnostic {
|
|
270
|
+
message: format!(
|
|
271
|
+
"Type '{}' is not assignable to type '{}'.",
|
|
272
|
+
show(t),
|
|
273
|
+
show(ann)
|
|
274
|
+
),
|
|
275
|
+
span: e.span(),
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
// Bind the *declared* type if annotated; otherwise the local is dynamic — bind `any`
|
|
281
|
+
// so later uses/reassignments are never flagged. (tish is gradual: an unannotated
|
|
282
|
+
// `let x = 5` may legitimately be reassigned `x = "s"`, unlike TS let-widening.)
|
|
283
|
+
let bound = type_ann.clone().unwrap_or_else(|| simple("any"));
|
|
284
|
+
self.define(name.as_ref(), bound);
|
|
285
|
+
}
|
|
286
|
+
// Comma-declarators: check + bind each declarator in the current scope.
|
|
287
|
+
Statement::Multi { statements, .. } => {
|
|
288
|
+
for st in statements {
|
|
289
|
+
self.check_stmt(st);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
Statement::VarDeclDestructure { init, .. } => {
|
|
293
|
+
self.synth(init);
|
|
294
|
+
}
|
|
295
|
+
Statement::ExprStmt { expr, .. } => {
|
|
296
|
+
self.synth(expr);
|
|
297
|
+
}
|
|
298
|
+
Statement::Return { value, .. } => {
|
|
299
|
+
let expected = self.ret_stack.last().cloned().flatten();
|
|
300
|
+
if let Some(e) = value {
|
|
301
|
+
let t = self.synth(e);
|
|
302
|
+
if let (Some(rt), Some(t)) = (&expected, &t) {
|
|
303
|
+
if !assignable(t, rt, &self.aliases) {
|
|
304
|
+
self.diags.push(TypeDiagnostic {
|
|
305
|
+
message: format!(
|
|
306
|
+
"Type '{}' is not assignable to the declared return type '{}'.",
|
|
307
|
+
show(t),
|
|
308
|
+
show(rt)
|
|
309
|
+
),
|
|
310
|
+
span: e.span(),
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
Statement::FunDecl {
|
|
317
|
+
params,
|
|
318
|
+
rest_param,
|
|
319
|
+
return_type,
|
|
320
|
+
body,
|
|
321
|
+
..
|
|
322
|
+
} => {
|
|
323
|
+
self.push();
|
|
324
|
+
for fp in params {
|
|
325
|
+
if let FunParam::Simple(tp) = fp {
|
|
326
|
+
if let Some(ann) = &tp.type_ann {
|
|
327
|
+
self.define(tp.name.as_ref(), ann.clone());
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
if let Some(rp) = rest_param {
|
|
332
|
+
if let Some(ann) = &rp.type_ann {
|
|
333
|
+
self.define(rp.name.as_ref(), ann.clone());
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
self.ret_stack.push(return_type.clone());
|
|
337
|
+
self.check_stmt(body);
|
|
338
|
+
self.ret_stack.pop();
|
|
339
|
+
self.pop();
|
|
340
|
+
}
|
|
341
|
+
Statement::Block { statements, .. } => self.check_block(statements),
|
|
342
|
+
Statement::If {
|
|
343
|
+
cond,
|
|
344
|
+
then_branch,
|
|
345
|
+
else_branch,
|
|
346
|
+
..
|
|
347
|
+
} => {
|
|
348
|
+
self.synth(cond);
|
|
349
|
+
self.check_stmt(then_branch);
|
|
350
|
+
if let Some(e) = else_branch {
|
|
351
|
+
self.check_stmt(e);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
Statement::While { cond, body, .. } | Statement::DoWhile { cond, body, .. } => {
|
|
355
|
+
self.synth(cond);
|
|
356
|
+
self.check_stmt(body);
|
|
357
|
+
}
|
|
358
|
+
Statement::For {
|
|
359
|
+
init,
|
|
360
|
+
cond,
|
|
361
|
+
update,
|
|
362
|
+
body,
|
|
363
|
+
..
|
|
364
|
+
} => {
|
|
365
|
+
self.push();
|
|
366
|
+
if let Some(i) = init {
|
|
367
|
+
self.check_stmt(i);
|
|
368
|
+
}
|
|
369
|
+
if let Some(c) = cond {
|
|
370
|
+
self.synth(c);
|
|
371
|
+
}
|
|
372
|
+
if let Some(u) = update {
|
|
373
|
+
self.synth(u);
|
|
374
|
+
}
|
|
375
|
+
self.check_stmt(body);
|
|
376
|
+
self.pop();
|
|
377
|
+
}
|
|
378
|
+
Statement::ForOf {
|
|
379
|
+
name,
|
|
380
|
+
iterable,
|
|
381
|
+
body,
|
|
382
|
+
..
|
|
383
|
+
} => {
|
|
384
|
+
self.push();
|
|
385
|
+
// Bind the loop var to the element type when the iterable is a known `T[]`.
|
|
386
|
+
if let Some(TypeAnnotation::Array(elem)) =
|
|
387
|
+
self.synth(iterable).map(|t| resolve(&t, &self.aliases, 0).clone())
|
|
388
|
+
{
|
|
389
|
+
self.define(name.as_ref(), *elem);
|
|
390
|
+
}
|
|
391
|
+
self.check_stmt(body);
|
|
392
|
+
self.pop();
|
|
393
|
+
}
|
|
394
|
+
Statement::Switch {
|
|
395
|
+
expr,
|
|
396
|
+
cases,
|
|
397
|
+
default_body,
|
|
398
|
+
..
|
|
399
|
+
} => {
|
|
400
|
+
self.synth(expr);
|
|
401
|
+
for (g, body) in cases {
|
|
402
|
+
if let Some(g) = g {
|
|
403
|
+
self.synth(g);
|
|
404
|
+
}
|
|
405
|
+
self.check_block(body);
|
|
406
|
+
}
|
|
407
|
+
if let Some(b) = default_body {
|
|
408
|
+
self.check_block(b);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
Statement::Try {
|
|
412
|
+
body,
|
|
413
|
+
catch_body,
|
|
414
|
+
finally_body,
|
|
415
|
+
..
|
|
416
|
+
} => {
|
|
417
|
+
self.check_stmt(body);
|
|
418
|
+
if let Some(b) = catch_body {
|
|
419
|
+
self.check_stmt(b);
|
|
420
|
+
}
|
|
421
|
+
if let Some(b) = finally_body {
|
|
422
|
+
self.check_stmt(b);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
Statement::Throw { value, .. } => {
|
|
426
|
+
self.synth(value);
|
|
427
|
+
}
|
|
428
|
+
_ => {}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// ── expression type synthesis (gradual) + nested call/assign checks ──────────────────────────
|
|
433
|
+
|
|
434
|
+
fn synth(&mut self, e: &Expr) -> Option<TypeAnnotation> {
|
|
435
|
+
match e {
|
|
436
|
+
Expr::Literal { value, .. } => Some(match value {
|
|
437
|
+
Literal::Number(_) => simple("number"),
|
|
438
|
+
Literal::String(_) => simple("string"),
|
|
439
|
+
Literal::Bool(_) => simple("boolean"),
|
|
440
|
+
Literal::Null => simple("null"),
|
|
441
|
+
}),
|
|
442
|
+
Expr::Ident { name, .. } => self.lookup(name.as_ref()),
|
|
443
|
+
Expr::Binary { left, op, right, .. } => {
|
|
444
|
+
let lt = self.synth(left);
|
|
445
|
+
let rt = self.synth(right);
|
|
446
|
+
bin_type(*op, lt.as_ref(), rt.as_ref())
|
|
447
|
+
}
|
|
448
|
+
Expr::Unary { op, operand, .. } => {
|
|
449
|
+
let t = self.synth(operand);
|
|
450
|
+
match op {
|
|
451
|
+
UnaryOp::Not => Some(simple("boolean")),
|
|
452
|
+
UnaryOp::Neg | UnaryOp::Pos => {
|
|
453
|
+
if t.as_ref().map(|x| is_named(x, "number")).unwrap_or(false) {
|
|
454
|
+
Some(simple("number"))
|
|
455
|
+
} else {
|
|
456
|
+
None
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
_ => None,
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
Expr::Call { callee, args, .. } => {
|
|
463
|
+
let arg_types: Vec<Option<TypeAnnotation>> = args
|
|
464
|
+
.iter()
|
|
465
|
+
.map(|a| match a {
|
|
466
|
+
CallArg::Expr(x) => self.synth(x),
|
|
467
|
+
CallArg::Spread(x) => {
|
|
468
|
+
self.synth(x);
|
|
469
|
+
None
|
|
470
|
+
}
|
|
471
|
+
})
|
|
472
|
+
.collect();
|
|
473
|
+
if let Expr::Ident { name, .. } = callee.as_ref() {
|
|
474
|
+
if let Some(sig) = self.sigs.get(name.as_ref()).cloned() {
|
|
475
|
+
for (i, pt) in sig.params.iter().enumerate() {
|
|
476
|
+
if let (Some(pt), Some(Some(at))) = (pt, arg_types.get(i)) {
|
|
477
|
+
if !assignable(at, pt, &self.aliases) {
|
|
478
|
+
let span = match &args[i] {
|
|
479
|
+
CallArg::Expr(x) | CallArg::Spread(x) => x.span(),
|
|
480
|
+
};
|
|
481
|
+
self.diags.push(TypeDiagnostic {
|
|
482
|
+
message: format!(
|
|
483
|
+
"Argument of type '{}' is not assignable to parameter of type '{}'.",
|
|
484
|
+
show(at),
|
|
485
|
+
show(pt)
|
|
486
|
+
),
|
|
487
|
+
span,
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
return sig.ret.clone();
|
|
493
|
+
}
|
|
494
|
+
} else {
|
|
495
|
+
self.synth(callee);
|
|
496
|
+
}
|
|
497
|
+
None
|
|
498
|
+
}
|
|
499
|
+
Expr::Member { object, prop, .. } => {
|
|
500
|
+
let ot = self.synth(object);
|
|
501
|
+
if let (Some(ot), MemberProp::Name { name, .. }) = (ot, prop) {
|
|
502
|
+
let resolved = resolve(&ot, &self.aliases, 0).clone();
|
|
503
|
+
match &resolved {
|
|
504
|
+
TypeAnnotation::Object(fields) => {
|
|
505
|
+
return fields
|
|
506
|
+
.iter()
|
|
507
|
+
.find(|(k, _)| k.as_ref() == name.as_ref())
|
|
508
|
+
.map(|(_, t)| t.clone());
|
|
509
|
+
}
|
|
510
|
+
TypeAnnotation::Array(_) if name.as_ref() == "length" => {
|
|
511
|
+
return Some(simple("number"));
|
|
512
|
+
}
|
|
513
|
+
TypeAnnotation::Simple(s)
|
|
514
|
+
if s.as_ref() == "string" && name.as_ref() == "length" =>
|
|
515
|
+
{
|
|
516
|
+
return Some(simple("number"));
|
|
517
|
+
}
|
|
518
|
+
_ => {}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
None
|
|
522
|
+
}
|
|
523
|
+
Expr::Index { object, index, .. } => {
|
|
524
|
+
let ot = self.synth(object);
|
|
525
|
+
self.synth(index);
|
|
526
|
+
if let Some(TypeAnnotation::Array(elem)) =
|
|
527
|
+
ot.map(|t| resolve(&t, &self.aliases, 0).clone())
|
|
528
|
+
{
|
|
529
|
+
return Some(*elem);
|
|
530
|
+
}
|
|
531
|
+
None
|
|
532
|
+
}
|
|
533
|
+
Expr::Assign { name, value, .. } => {
|
|
534
|
+
let vt = self.synth(value);
|
|
535
|
+
if let (Some(target), Some(vt)) = (self.lookup(name.as_ref()), &vt) {
|
|
536
|
+
if !assignable(vt, &target, &self.aliases) {
|
|
537
|
+
self.diags.push(TypeDiagnostic {
|
|
538
|
+
message: format!(
|
|
539
|
+
"Type '{}' is not assignable to type '{}'.",
|
|
540
|
+
show(vt),
|
|
541
|
+
show(&target)
|
|
542
|
+
),
|
|
543
|
+
span: value.span(),
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
return Some(target);
|
|
547
|
+
}
|
|
548
|
+
vt
|
|
549
|
+
}
|
|
550
|
+
Expr::CompoundAssign { value, .. } | Expr::LogicalAssign { value, .. } => {
|
|
551
|
+
self.synth(value);
|
|
552
|
+
None
|
|
553
|
+
}
|
|
554
|
+
Expr::Conditional {
|
|
555
|
+
cond,
|
|
556
|
+
then_branch,
|
|
557
|
+
else_branch,
|
|
558
|
+
..
|
|
559
|
+
} => {
|
|
560
|
+
self.synth(cond);
|
|
561
|
+
let t = self.synth(then_branch);
|
|
562
|
+
let f = self.synth(else_branch);
|
|
563
|
+
match (t, f) {
|
|
564
|
+
(Some(a), Some(b)) if a == b => Some(a),
|
|
565
|
+
_ => None,
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
Expr::Array { elements, .. } => {
|
|
569
|
+
let mut elem: Option<TypeAnnotation> = None;
|
|
570
|
+
for el in elements {
|
|
571
|
+
match el {
|
|
572
|
+
ArrayElement::Expr(x) => {
|
|
573
|
+
let t = self.synth(x)?;
|
|
574
|
+
match &elem {
|
|
575
|
+
None => elem = Some(t),
|
|
576
|
+
Some(p) if *p != t => return None,
|
|
577
|
+
_ => {}
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
ArrayElement::Spread(x) => {
|
|
581
|
+
self.synth(x);
|
|
582
|
+
return None;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
elem.map(|t| TypeAnnotation::Array(Box::new(t)))
|
|
587
|
+
}
|
|
588
|
+
Expr::Object { props, .. } => {
|
|
589
|
+
let mut fields = Vec::new();
|
|
590
|
+
for p in props {
|
|
591
|
+
match p {
|
|
592
|
+
ObjectProp::KeyValue(k, v) => {
|
|
593
|
+
let t = self.synth(v)?;
|
|
594
|
+
fields.push((k.clone(), t));
|
|
595
|
+
}
|
|
596
|
+
ObjectProp::Spread(v) => {
|
|
597
|
+
self.synth(v);
|
|
598
|
+
return None;
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
Some(TypeAnnotation::Object(fields))
|
|
603
|
+
}
|
|
604
|
+
_ => None,
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
/// Result type of a binary op given (optional) operand types. Mirrors the runtime/codegen rules,
|
|
610
|
+
/// gradual: any uncertainty -> `None`.
|
|
611
|
+
fn bin_type(
|
|
612
|
+
op: BinOp,
|
|
613
|
+
lt: Option<&TypeAnnotation>,
|
|
614
|
+
rt: Option<&TypeAnnotation>,
|
|
615
|
+
) -> Option<TypeAnnotation> {
|
|
616
|
+
let both = |n: &str| {
|
|
617
|
+
lt.map(|t| is_named(t, n)).unwrap_or(false) && rt.map(|t| is_named(t, n)).unwrap_or(false)
|
|
618
|
+
};
|
|
619
|
+
use BinOp::*;
|
|
620
|
+
match op {
|
|
621
|
+
Add => {
|
|
622
|
+
if both("number") {
|
|
623
|
+
Some(simple("number"))
|
|
624
|
+
} else if both("string") {
|
|
625
|
+
Some(simple("string"))
|
|
626
|
+
} else {
|
|
627
|
+
None
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
Sub | Mul | Div | Mod | Pow => {
|
|
631
|
+
if both("number") {
|
|
632
|
+
Some(simple("number"))
|
|
633
|
+
} else {
|
|
634
|
+
None
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
Lt | Le | Gt | Ge | StrictEq | StrictNe => {
|
|
638
|
+
if both("number") || both("string") || both("boolean") {
|
|
639
|
+
Some(simple("boolean"))
|
|
640
|
+
} else {
|
|
641
|
+
None
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
And | Or => {
|
|
645
|
+
if both("boolean") {
|
|
646
|
+
Some(simple("boolean"))
|
|
647
|
+
} else {
|
|
648
|
+
None
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
_ => None,
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
/// Run `f` over the child statement-blocks of `s` (loop/if/fn/switch/try bodies) for the
|
|
656
|
+
/// recursive pre-passes. Single-statement bodies are passed as one-element slices.
|
|
657
|
+
fn for_each_child_block(s: &Statement, f: &mut dyn FnMut(&[Statement])) {
|
|
658
|
+
match s {
|
|
659
|
+
Statement::Block { statements, .. } | Statement::Multi { statements, .. } => f(statements),
|
|
660
|
+
Statement::If {
|
|
661
|
+
then_branch,
|
|
662
|
+
else_branch,
|
|
663
|
+
..
|
|
664
|
+
} => {
|
|
665
|
+
f(std::slice::from_ref(then_branch));
|
|
666
|
+
if let Some(e) = else_branch {
|
|
667
|
+
f(std::slice::from_ref(e));
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
Statement::While { body, .. }
|
|
671
|
+
| Statement::DoWhile { body, .. }
|
|
672
|
+
| Statement::ForOf { body, .. }
|
|
673
|
+
| Statement::FunDecl { body, .. } => f(std::slice::from_ref(body)),
|
|
674
|
+
Statement::For { init, body, .. } => {
|
|
675
|
+
if let Some(i) = init {
|
|
676
|
+
f(std::slice::from_ref(i));
|
|
677
|
+
}
|
|
678
|
+
f(std::slice::from_ref(body));
|
|
679
|
+
}
|
|
680
|
+
Statement::Switch {
|
|
681
|
+
cases,
|
|
682
|
+
default_body,
|
|
683
|
+
..
|
|
684
|
+
} => {
|
|
685
|
+
for (_, body) in cases {
|
|
686
|
+
f(body);
|
|
687
|
+
}
|
|
688
|
+
if let Some(b) = default_body {
|
|
689
|
+
f(b);
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
Statement::Try {
|
|
693
|
+
body,
|
|
694
|
+
catch_body,
|
|
695
|
+
finally_body,
|
|
696
|
+
..
|
|
697
|
+
} => {
|
|
698
|
+
f(std::slice::from_ref(body));
|
|
699
|
+
if let Some(b) = catch_body {
|
|
700
|
+
f(std::slice::from_ref(b));
|
|
701
|
+
}
|
|
702
|
+
if let Some(b) = finally_body {
|
|
703
|
+
f(std::slice::from_ref(b));
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
_ => {}
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
#[cfg(test)]
|
|
711
|
+
mod tests {
|
|
712
|
+
use super::*;
|
|
713
|
+
use tishlang_parser::parse;
|
|
714
|
+
|
|
715
|
+
fn diags(src: &str) -> Vec<String> {
|
|
716
|
+
let prog = parse(src).unwrap();
|
|
717
|
+
check_program(&prog).into_iter().map(|d| d.message).collect()
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
#[test]
|
|
721
|
+
fn ok_programs_have_no_diagnostics() {
|
|
722
|
+
for src in [
|
|
723
|
+
"let x: number = 5",
|
|
724
|
+
"let s: string = \"hi\"",
|
|
725
|
+
"let b: boolean = true",
|
|
726
|
+
"let x: number = 1 + 2 * 3",
|
|
727
|
+
"let s: string = \"a\" + \"b\"",
|
|
728
|
+
"fn f(a: number): number { return a + 1 }",
|
|
729
|
+
"fn f(a: number) {} f(5)",
|
|
730
|
+
"let x: number = unknownCall()", // gradual: unknown -> no error
|
|
731
|
+
"let x: any = \"anything\"", // any accepts anything
|
|
732
|
+
"type P = { x: number, y: number }\nlet p: P = { x: 1, y: 2 }",
|
|
733
|
+
"let xs: number[] = [1, 2, 3]",
|
|
734
|
+
"fn f(a: number): number { return a }\nlet n: number = f(2)",
|
|
735
|
+
] {
|
|
736
|
+
assert_eq!(diags(src), Vec::<String>::new(), "unexpected diagnostics for: {src}");
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
#[test]
|
|
741
|
+
fn flags_decl_mismatch() {
|
|
742
|
+
assert_eq!(diags("let x: number = \"s\"").len(), 1);
|
|
743
|
+
assert_eq!(diags("let s: string = 42").len(), 1);
|
|
744
|
+
assert_eq!(diags("let b: boolean = 1").len(), 1);
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
#[test]
|
|
748
|
+
fn flags_return_mismatch() {
|
|
749
|
+
assert_eq!(diags("fn f(): number { return \"s\" }").len(), 1);
|
|
750
|
+
assert_eq!(diags("fn f(): string { return 5 }").len(), 1);
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
#[test]
|
|
754
|
+
fn flags_call_arg_mismatch() {
|
|
755
|
+
assert_eq!(diags("fn f(a: number) {}\nf(\"s\")").len(), 1);
|
|
756
|
+
assert_eq!(diags("fn f(a: number, b: string) {}\nf(1, 2)").len(), 1);
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
#[test]
|
|
760
|
+
fn flags_reassignment_mismatch() {
|
|
761
|
+
assert_eq!(diags("let x: number = 5\nx = \"s\"").len(), 1);
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
#[test]
|
|
765
|
+
fn flags_struct_field_mismatch() {
|
|
766
|
+
assert_eq!(diags("let p: { x: number } = { x: \"s\" }").len(), 1);
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
#[test]
|
|
770
|
+
fn gradual_no_false_positive_on_unknown_arg() {
|
|
771
|
+
// arg type unknown (param of an unknown fn) -> no error
|
|
772
|
+
assert_eq!(diags("fn f(a: number) {}\nf(someUnknown())"), Vec::<String>::new());
|
|
773
|
+
}
|
|
774
|
+
}
|