@tishlang/tish-format 1.0.13 → 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.
Files changed (108) hide show
  1. package/Cargo.toml +2 -0
  2. package/bin/tish-format +0 -0
  3. package/crates/js_to_tish/src/transform/expr.rs +1 -0
  4. package/crates/tish/Cargo.toml +10 -2
  5. package/crates/tish/build.rs +21 -0
  6. package/crates/tish/src/cli_help.rs +15 -4
  7. package/crates/tish/src/main.rs +93 -21
  8. package/crates/tish/src/repl_completion.rs +0 -1
  9. package/crates/tish/tests/error_source_location.rs +36 -0
  10. package/crates/tish/tests/fixtures/runtime_error_location.tish +5 -0
  11. package/crates/tish/tests/fixtures/trycatch_runtime_errors.tish +15 -0
  12. package/crates/tish/tests/fixtures/tty_capability.tish +9 -0
  13. package/crates/tish/tests/integration_test.rs +402 -91
  14. package/crates/tish/tests/trycatch_runtime_errors.rs +45 -0
  15. package/crates/tish/tests/tty_capability.rs +43 -0
  16. package/crates/tish_ast/src/ast.rs +37 -8
  17. package/crates/tish_builtins/Cargo.toml +2 -0
  18. package/crates/tish_builtins/src/array.rs +375 -13
  19. package/crates/tish_builtins/src/collections.rs +481 -0
  20. package/crates/tish_builtins/src/construct.rs +59 -19
  21. package/crates/tish_builtins/src/date.rs +538 -0
  22. package/crates/tish_builtins/src/globals.rs +86 -6
  23. package/crates/tish_builtins/src/iterator.rs +129 -0
  24. package/crates/tish_builtins/src/lib.rs +5 -0
  25. package/crates/tish_builtins/src/number.rs +96 -0
  26. package/crates/tish_builtins/src/object.rs +2 -2
  27. package/crates/tish_builtins/src/string.rs +19 -20
  28. package/crates/tish_builtins/src/symbol.rs +1 -1
  29. package/crates/tish_builtins/src/typedarrays.rs +298 -0
  30. package/crates/tish_bytecode/src/chunk.rs +69 -1
  31. package/crates/tish_bytecode/src/compiler.rs +933 -89
  32. package/crates/tish_bytecode/src/encoding.rs +2 -0
  33. package/crates/tish_bytecode/src/lib.rs +2 -1
  34. package/crates/tish_bytecode/src/opcode.rs +47 -4
  35. package/crates/tish_bytecode/src/serialize.rs +31 -1
  36. package/crates/tish_compile/Cargo.toml +1 -0
  37. package/crates/tish_compile/src/check.rs +774 -0
  38. package/crates/tish_compile/src/codegen.rs +2334 -349
  39. package/crates/tish_compile/src/infer.rs +1395 -6
  40. package/crates/tish_compile/src/lib.rs +50 -8
  41. package/crates/tish_compile/src/resolve.rs +584 -21
  42. package/crates/tish_compile/src/types.rs +106 -2
  43. package/crates/tish_compile_js/src/codegen.rs +67 -0
  44. package/crates/tish_compile_js/src/tests_jsx.rs +64 -0
  45. package/crates/tish_core/Cargo.toml +7 -1
  46. package/crates/tish_core/src/console_style.rs +11 -1
  47. package/crates/tish_core/src/json.rs +81 -38
  48. package/crates/tish_core/src/lib.rs +3 -0
  49. package/crates/tish_core/src/shape.rs +85 -0
  50. package/crates/tish_core/src/value.rs +679 -25
  51. package/crates/tish_core/src/vmref.rs +13 -8
  52. package/crates/tish_cranelift/src/link.rs +17 -4
  53. package/crates/tish_cranelift_runtime/Cargo.toml +1 -0
  54. package/crates/tish_eval/Cargo.toml +6 -0
  55. package/crates/tish_eval/src/eval.rs +665 -117
  56. package/crates/tish_eval/src/http.rs +4 -1
  57. package/crates/tish_eval/src/natives.rs +165 -13
  58. package/crates/tish_eval/src/value.rs +31 -13
  59. package/crates/tish_eval/src/value_convert.rs +10 -4
  60. package/crates/tish_ffi/Cargo.toml +26 -0
  61. package/crates/tish_ffi/src/lib.rs +518 -0
  62. package/crates/tish_ffi/tests/fixtures/testmod/Cargo.toml +18 -0
  63. package/crates/tish_ffi/tests/fixtures/testmod/src/lib.rs +46 -0
  64. package/crates/tish_ffi/tests/loader.rs +65 -0
  65. package/crates/tish_fmt/Cargo.toml +1 -1
  66. package/crates/tish_fmt/src/lib.rs +61 -5
  67. package/crates/tish_lexer/src/lib.rs +397 -9
  68. package/crates/tish_lexer/src/token.rs +7 -0
  69. package/crates/tish_lint/src/lib.rs +2 -10
  70. package/crates/tish_lsp/src/import_goto.rs +2 -0
  71. package/crates/tish_lsp/src/main.rs +439 -26
  72. package/crates/tish_native/src/build.rs +55 -1
  73. package/crates/tish_opt/src/lib.rs +126 -23
  74. package/crates/tish_parser/src/lib.rs +55 -1
  75. package/crates/tish_parser/src/parser.rs +456 -34
  76. package/crates/tish_pg/src/lib.rs +3 -3
  77. package/crates/tish_resolve/src/lib.rs +99 -59
  78. package/crates/tish_runtime/Cargo.toml +4 -0
  79. package/crates/tish_runtime/src/http.rs +66 -17
  80. package/crates/tish_runtime/src/http_fetch.rs +29 -8
  81. package/crates/tish_runtime/src/http_hyper.rs +25 -2
  82. package/crates/tish_runtime/src/lib.rs +299 -44
  83. package/crates/tish_runtime/src/promise.rs +328 -18
  84. package/crates/tish_runtime/src/timers.rs +13 -7
  85. package/crates/tish_runtime/src/tty.rs +226 -0
  86. package/crates/tish_runtime/src/ws.rs +35 -18
  87. package/crates/tish_runtime/tests/fetch_readable_stream.rs +2 -2
  88. package/crates/tish_ui/src/jsx.rs +10 -0
  89. package/crates/tish_ui/src/runtime/hooks.rs +19 -15
  90. package/crates/tish_ui/src/runtime/mod.rs +15 -12
  91. package/crates/tish_vm/Cargo.toml +14 -1
  92. package/crates/tish_vm/src/jit.rs +1050 -0
  93. package/crates/tish_vm/src/lib.rs +2 -0
  94. package/crates/tish_vm/src/vm.rs +1546 -202
  95. package/crates/tish_vm/tests/concurrent_shared_state.rs +140 -0
  96. package/crates/tish_wasm/src/lib.rs +6 -2
  97. package/crates/tish_wasm_runtime/src/gpu.rs +17 -1
  98. package/crates/tishlang_cargo_bindgen/src/classify.rs +1 -3
  99. package/crates/tishlang_cargo_bindgen/src/lib.rs +2 -2
  100. package/crates/tishlang_cargo_bindgen/src/metadata.rs +1 -1
  101. package/justfile +8 -0
  102. package/package.json +2 -2
  103. package/platform/darwin-arm64/tish-fmt +0 -0
  104. package/platform/darwin-x64/tish-fmt +0 -0
  105. package/platform/linux-arm64/tish-fmt +0 -0
  106. package/platform/linux-x64/tish-fmt +0 -0
  107. package/platform/win32-x64/tish-fmt.exe +0 -0
  108. 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
+ }