@tishlang/tish 2.2.4 → 2.2.7

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/bin/tish CHANGED
Binary file
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "tishlang"
3
- version = "2.2.4"
3
+ version = "2.2.7"
4
4
  edition = "2021"
5
5
  description = "Tish CLI - run, REPL, compile to native"
6
6
  license-file = { workspace = true }
@@ -2,7 +2,7 @@
2
2
 
3
3
  use std::sync::Arc;
4
4
 
5
- #[derive(Debug, Clone, Copy, PartialEq)]
5
+ #[derive(Debug, Clone, Copy, PartialEq, Default)]
6
6
  pub struct Span {
7
7
  pub start: (usize, usize), // line, col
8
8
  pub end: (usize, usize),
@@ -11,8 +11,11 @@ pub struct Span {
11
11
  /// Type annotation for variables, parameters, and return types.
12
12
  #[derive(Debug, Clone, PartialEq)]
13
13
  pub enum TypeAnnotation {
14
- /// Primitive types: number, string, boolean, null
15
- Simple(Arc<str>),
14
+ /// Primitive types and type-name references: `number`, `string`, `MyAlias`. The span locates
15
+ /// the name token in source so type-alias rename / find-references can edit every `: T` use
16
+ /// (synthesized types — monomorphized generics, builtins, postfix `?` null — use a zero span
17
+ /// and never match a user alias). Per AST convention, spans participate in PartialEq.
18
+ Simple(Arc<str>, Span),
16
19
  /// Array type: T[]
17
20
  Array(Box<TypeAnnotation>),
18
21
  /// Object type: { key: Type, ... }
@@ -490,9 +490,46 @@ pub fn copy_binary_to_output(binary: &Path, output_path: &Path) -> Result<(), St
490
490
  }
491
491
  fs::copy(binary, output_path)
492
492
  .map_err(|e| format!("Cannot copy to {}: {}", output_path.display(), e))?;
493
+ resign_macos_adhoc(output_path);
493
494
  Ok(())
494
495
  }
495
496
 
497
+ /// Replace the linker-produced ad-hoc signature with a plain ad-hoc one on macOS.
498
+ ///
499
+ /// On Apple Silicon, AMFI SIGKILLs a freshly linked binary whose ad-hoc signature carries the
500
+ /// `linker-signed` flag (instant exit 137, no output — looks like a crash/hang), even though
501
+ /// `codesign -v` reports it valid (#219). A standard `codesign --sign - --force` replaces it with a
502
+ /// kernel-acceptable signature. Best-effort: static archives are skipped, and a failure (codesign
503
+ /// absent, or a non-Mach-O output) is non-fatal — the binary is then no worse off than before.
504
+ #[cfg(target_os = "macos")]
505
+ fn resign_macos_adhoc(output_path: &Path) {
506
+ // Static archives (`.a`) are not code-signed.
507
+ if output_path.extension().is_some_and(|e| e == "a") {
508
+ return;
509
+ }
510
+ match Command::new("codesign")
511
+ .args(["--sign", "-", "--force"])
512
+ .arg(output_path)
513
+ .output()
514
+ {
515
+ Ok(out) if out.status.success() => {}
516
+ Ok(out) => eprintln!(
517
+ "warning: codesign --sign - failed for {} ({}); the binary may be SIGKILLed at launch on Apple Silicon — re-sign with `codesign -s - -f {}`",
518
+ output_path.display(),
519
+ String::from_utf8_lossy(&out.stderr).trim(),
520
+ output_path.display()
521
+ ),
522
+ Err(e) => eprintln!(
523
+ "warning: could not run codesign for {} ({e}); if the binary is killed at launch, re-sign with `codesign -s - -f {}`",
524
+ output_path.display(),
525
+ output_path.display()
526
+ ),
527
+ }
528
+ }
529
+
530
+ #[cfg(not(target_os = "macos"))]
531
+ fn resign_macos_adhoc(_output_path: &Path) {}
532
+
496
533
  #[cfg(test)]
497
534
  mod tests {
498
535
  use super::*;
@@ -29,6 +29,7 @@ fn lit_base(lit: &TypeLiteral) -> TypeAnnotation {
29
29
  TypeLiteral::Bool(_) => "boolean",
30
30
  }
31
31
  .into(),
32
+ tishlang_ast::Span::default(),
32
33
  )
33
34
  }
34
35
 
@@ -70,19 +71,19 @@ pub fn check_program(program: &Program) -> Vec<TypeDiagnostic> {
70
71
  // ── helpers: type constructors / predicates ─────────────────────────────────────────────────
71
72
 
72
73
  fn simple(s: &str) -> TypeAnnotation {
73
- TypeAnnotation::Simple(s.into())
74
+ TypeAnnotation::Simple(s.into(), tishlang_ast::Span::default())
74
75
  }
75
76
  fn is_any(ann: &TypeAnnotation) -> bool {
76
- matches!(ann, TypeAnnotation::Simple(s) if s.as_ref() == "any")
77
+ matches!(ann, TypeAnnotation::Simple(s, _) if s.as_ref() == "any")
77
78
  }
78
79
  fn is_named(ann: &TypeAnnotation, n: &str) -> bool {
79
- matches!(ann, TypeAnnotation::Simple(s) if s.as_ref() == n)
80
+ matches!(ann, TypeAnnotation::Simple(s, _) if s.as_ref() == n)
80
81
  }
81
82
 
82
83
  /// Display a type for diagnostics (close to the source syntax).
83
84
  fn show(ann: &TypeAnnotation) -> String {
84
85
  match ann {
85
- TypeAnnotation::Simple(s) => s.to_string(),
86
+ TypeAnnotation::Simple(s, _) => s.to_string(),
86
87
  TypeAnnotation::Array(t) => format!("{}[]", show(t)),
87
88
  TypeAnnotation::Object(fs) => {
88
89
  let inner: Vec<String> = fs.iter().map(|(k, t)| format!("{}: {}", k, show(t))).collect();
@@ -116,7 +117,7 @@ fn resolve<'a>(
116
117
  if depth > 8 {
117
118
  return ann;
118
119
  }
119
- if let TypeAnnotation::Simple(s) = ann {
120
+ if let TypeAnnotation::Simple(s, _) = ann {
120
121
  if let Some(t) = aliases.get(s.as_ref()) {
121
122
  return resolve(t, aliases, depth + 1);
122
123
  }
@@ -138,12 +139,12 @@ fn assignable(
138
139
  }
139
140
  // `null`/`void`/`undefined` are leniently compatible (tish uses `null` for optionals; checking
140
141
  // 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
+ if matches!(a, TypeAnnotation::Simple(s, _) if matches!(s.as_ref(), "null" | "void" | "undefined")) {
142
143
  return true;
143
144
  }
144
145
  use TypeAnnotation::*;
145
146
  match (a, e) {
146
- (Simple(x), Simple(y)) => {
147
+ (Simple(x, _), Simple(y, _)) => {
147
148
  // Strict only among the three scalar primitives; any user-defined / unresolved name is
148
149
  // treated as compatible.
149
150
  let strict = |s: &str| matches!(s, "number" | "string" | "boolean");
@@ -155,7 +156,7 @@ fn assignable(
155
156
  }
156
157
  (Array(ax), Array(ey)) => assignable(ax, ey, aliases),
157
158
  // array vs non-array (after alias/any resolution) is a clear mismatch
158
- (Array(_), Simple(_)) | (Simple(_), Array(_)) => false,
159
+ (Array(_), Simple(_, _)) | (Simple(_, _), Array(_)) => false,
159
160
  (Object(af), Object(ef)) => ef.iter().all(|(k, et)| {
160
161
  af.iter()
161
162
  .find(|(ak, _)| ak.as_ref() == k.as_ref())
@@ -510,7 +511,7 @@ impl CheckCtx {
510
511
  TypeAnnotation::Array(_) if name.as_ref() == "length" => {
511
512
  return Some(simple("number"));
512
513
  }
513
- TypeAnnotation::Simple(s)
514
+ TypeAnnotation::Simple(s, _)
514
515
  if s.as_ref() == "string" && name.as_ref() == "length" =>
515
516
  {
516
517
  return Some(simple("number"));
@@ -52,15 +52,15 @@ impl InferCtx {
52
52
  }
53
53
 
54
54
  fn is_number(ann: &TypeAnnotation) -> bool {
55
- matches!(ann, TypeAnnotation::Simple(s) if s.as_ref() == "number")
55
+ matches!(ann, TypeAnnotation::Simple(s, _) if s.as_ref() == "number")
56
56
  }
57
57
 
58
58
  fn is_string(ann: &TypeAnnotation) -> bool {
59
- matches!(ann, TypeAnnotation::Simple(s) if s.as_ref() == "string")
59
+ matches!(ann, TypeAnnotation::Simple(s, _) if s.as_ref() == "string")
60
60
  }
61
61
 
62
62
  fn is_bool(ann: &TypeAnnotation) -> bool {
63
- matches!(ann, TypeAnnotation::Simple(s) if s.as_ref() == "boolean")
63
+ matches!(ann, TypeAnnotation::Simple(s, _) if s.as_ref() == "boolean")
64
64
  }
65
65
 
66
66
  /// Element type of an array literal of uniform native scalars (number/string/boolean), for
@@ -91,15 +91,15 @@ fn infer_array_elem(elements: &[tishlang_ast::ArrayElement], ctx: &InferCtx) ->
91
91
  }
92
92
 
93
93
  fn number_ann() -> TypeAnnotation {
94
- TypeAnnotation::Simple("number".into())
94
+ TypeAnnotation::Simple("number".into(), tishlang_ast::Span::default())
95
95
  }
96
96
 
97
97
  fn string_ann() -> TypeAnnotation {
98
- TypeAnnotation::Simple("string".into())
98
+ TypeAnnotation::Simple("string".into(), tishlang_ast::Span::default())
99
99
  }
100
100
 
101
101
  fn bool_ann() -> TypeAnnotation {
102
- TypeAnnotation::Simple("boolean".into())
102
+ TypeAnnotation::Simple("boolean".into(), tishlang_ast::Span::default())
103
103
  }
104
104
 
105
105
  /// Infer the `TypeAnnotation` for an expression, if unambiguous.
@@ -237,7 +237,7 @@ fn pi_stmt(s: Statement) -> Statement {
237
237
  && tp.default.is_none()
238
238
  && nus_stmt(&body, tp.name.as_ref(), &nums)
239
239
  {
240
- tp.type_ann = Some(TypeAnnotation::Simple(std::sync::Arc::from("number")));
240
+ tp.type_ann = Some(TypeAnnotation::Simple(std::sync::Arc::from("number"), tishlang_ast::Span::default()));
241
241
  }
242
242
  FunParam::Simple(tp)
243
243
  }
@@ -612,7 +612,7 @@ impl StructRegistry {
612
612
 
613
613
  fn type_canon(t: &TypeAnnotation) -> String {
614
614
  match t {
615
- TypeAnnotation::Simple(s) => s.to_string(),
615
+ TypeAnnotation::Simple(s, _) => s.to_string(),
616
616
  TypeAnnotation::Object(fields) => format!(
617
617
  "{{{}}}",
618
618
  fields
@@ -637,7 +637,7 @@ fn infer_object_shape(
637
637
  tishlang_ast::ObjectProp::KeyValue(k, v) => {
638
638
  let ty = infer_expr_type(v, ctx)?;
639
639
  // Only primitive field types in this conservative version.
640
- if !matches!(&ty, TypeAnnotation::Simple(s)
640
+ if !matches!(&ty, TypeAnnotation::Simple(s, _)
641
641
  if matches!(s.as_ref(), "number" | "string" | "boolean"))
642
642
  {
643
643
  return None;
@@ -776,12 +776,12 @@ fn si_block(stmts: Vec<Statement>, reg: &mut StructRegistry, ctx: &mut InferCtx)
776
776
  // Sound: every later use in this block must be a literal-key read.
777
777
  if uses_are_struct_safe(name.as_ref(), &keys, &stmts[i + 1..]) {
778
778
  let alias = reg.intern(&fields);
779
- ctx.define(name.as_ref(), TypeAnnotation::Simple(alias.as_str().into()));
779
+ ctx.define(name.as_ref(), TypeAnnotation::Simple(alias.as_str().into(), tishlang_ast::Span::default()));
780
780
  out.push(Statement::VarDecl {
781
781
  name: name.clone(),
782
782
  name_span: *name_span,
783
783
  mutable: *mutable,
784
- type_ann: Some(TypeAnnotation::Simple(alias.as_str().into())),
784
+ type_ann: Some(TypeAnnotation::Simple(alias.as_str().into(), tishlang_ast::Span::default())),
785
785
  init: stmt_init_clone(stmt),
786
786
  span: *span,
787
787
  });
@@ -1642,7 +1642,7 @@ mod param_infer_tests {
1642
1642
  if let FunParam::Simple(tp) = p {
1643
1643
  if tp.name.as_ref() == param {
1644
1644
  return tp.type_ann.as_ref().map(|a| match a {
1645
- TypeAnnotation::Simple(s) => s.to_string(),
1645
+ TypeAnnotation::Simple(s, _) => s.to_string(),
1646
1646
  _ => "<complex>".to_string(),
1647
1647
  });
1648
1648
  }
@@ -62,7 +62,7 @@ impl RustType {
62
62
  aliases: &HashMap<String, RustType>,
63
63
  ) -> Self {
64
64
  match ann {
65
- TypeAnnotation::Simple(name) => match name.as_ref() {
65
+ TypeAnnotation::Simple(name, _) => match name.as_ref() {
66
66
  "number" => RustType::F64,
67
67
  "string" => RustType::String,
68
68
  "boolean" | "bool" => RustType::Bool,
@@ -111,10 +111,10 @@ impl RustType {
111
111
  if types.len() == 2 {
112
112
  let has_null = types
113
113
  .iter()
114
- .any(|t| matches!(t, TypeAnnotation::Simple(s) if s.as_ref() == "null"));
114
+ .any(|t| matches!(t, TypeAnnotation::Simple(s, _) if s.as_ref() == "null"));
115
115
  if has_null {
116
116
  let non_null = types.iter().find(
117
- |t| !matches!(t, TypeAnnotation::Simple(s) if s.as_ref() == "null"),
117
+ |t| !matches!(t, TypeAnnotation::Simple(s, _) if s.as_ref() == "null"),
118
118
  );
119
119
  if let Some(inner) = non_null {
120
120
  return RustType::Option(Box::new(Self::from_annotation_with_aliases(
@@ -532,22 +532,22 @@ mod tests {
532
532
  #[test]
533
533
  fn test_simple_types() {
534
534
  assert_eq!(
535
- RustType::from_annotation(&TypeAnnotation::Simple("number".into())),
535
+ RustType::from_annotation(&TypeAnnotation::Simple("number".into(), tishlang_ast::Span::default())),
536
536
  RustType::F64
537
537
  );
538
538
  assert_eq!(
539
- RustType::from_annotation(&TypeAnnotation::Simple("string".into())),
539
+ RustType::from_annotation(&TypeAnnotation::Simple("string".into(), tishlang_ast::Span::default())),
540
540
  RustType::String
541
541
  );
542
542
  assert_eq!(
543
- RustType::from_annotation(&TypeAnnotation::Simple("boolean".into())),
543
+ RustType::from_annotation(&TypeAnnotation::Simple("boolean".into(), tishlang_ast::Span::default())),
544
544
  RustType::Bool
545
545
  );
546
546
  }
547
547
 
548
548
  #[test]
549
549
  fn test_array_type() {
550
- let arr_type = TypeAnnotation::Array(Box::new(TypeAnnotation::Simple("number".into())));
550
+ let arr_type = TypeAnnotation::Array(Box::new(TypeAnnotation::Simple("number".into(), tishlang_ast::Span::default())));
551
551
  assert_eq!(
552
552
  RustType::from_annotation(&arr_type),
553
553
  RustType::Vec(Box::new(RustType::F64))
@@ -557,8 +557,8 @@ mod tests {
557
557
  #[test]
558
558
  fn test_nullable_type() {
559
559
  let nullable = TypeAnnotation::Union(vec![
560
- TypeAnnotation::Simple("string".into()),
561
- TypeAnnotation::Simple("null".into()),
560
+ TypeAnnotation::Simple("string".into(), tishlang_ast::Span::default()),
561
+ TypeAnnotation::Simple("null".into(), tishlang_ast::Span::default()),
562
562
  ]);
563
563
  assert_eq!(
564
564
  RustType::from_annotation(&nullable),
@@ -1046,7 +1046,7 @@ impl Printer {
1046
1046
 
1047
1047
  fn type_ann(&mut self, t: &TypeAnnotation) {
1048
1048
  match t {
1049
- TypeAnnotation::Simple(s) => self.buf.push_str(s.as_ref()),
1049
+ TypeAnnotation::Simple(s, _) => self.buf.push_str(s.as_ref()),
1050
1050
  TypeAnnotation::Array(inner) => {
1051
1051
  self.type_ann(inner);
1052
1052
  self.buf.push_str("[]");
@@ -19,7 +19,7 @@ tishlang_fmt = { path = "../tish_fmt", version = ">=0.1" }
19
19
  tishlang_lint = { path = "../tish_lint", version = ">=0.1" }
20
20
  tishlang_resolve = { path = "../tish_resolve", version = ">=0.1" }
21
21
  tower-lsp = "0.20"
22
- tokio = { version = "1", features = ["rt-multi-thread", "macros", "io-std"] }
22
+ tokio = { version = "1", features = ["rt-multi-thread", "macros", "io-std", "time"] }
23
23
  serde_json = "1"
24
24
  regex = "1"
25
25
  walkdir = "2"
@@ -29,6 +29,10 @@ mod import_goto;
29
29
  struct Backend {
30
30
  client: Client,
31
31
  docs: Arc<RwLock<HashMap<Url, String>>>,
32
+ /// Monotonic per-document edit counter. did_change bumps it and a debounced task only
33
+ /// publishes diagnostics if its edit is still the latest — so rapid keystrokes coalesce and
34
+ /// superseded recomputes are dropped (the analysis pipeline is comparatively expensive).
35
+ edit_seq: Arc<RwLock<HashMap<Url, u64>>>,
32
36
  roots: Arc<RwLock<Vec<PathBuf>>>,
33
37
  /// `(project_root, cargo:spec)` → resolved dependency source root (for `cargo metadata` / registry).
34
38
  cargo_src_cache: Arc<RwLock<HashMap<(PathBuf, String), PathBuf>>>,
@@ -44,6 +48,7 @@ async fn main() {
44
48
  let (service, socket) = LspService::new(|client| Backend {
45
49
  client,
46
50
  docs: Arc::new(RwLock::new(HashMap::new())),
51
+ edit_seq: Arc::new(RwLock::new(HashMap::new())),
47
52
  roots: Arc::new(RwLock::new(Vec::new())),
48
53
  cargo_src_cache: Arc::new(RwLock::new(HashMap::new())),
49
54
  tishlang_source_root: Arc::new(RwLock::new(None)),
@@ -119,6 +124,15 @@ fn document_symbol(
119
124
  selection_range: Range,
120
125
  children: Option<Vec<DocumentSymbol>>,
121
126
  ) -> DocumentSymbol {
127
+ // LSP spec: selectionRange must be contained in range, or VS Code rejects the whole document
128
+ // outline ("selectionRange must be contained in fullRange"). Some declaration spans are
129
+ // unset/degenerate (e.g. a top-level VarDecl's span defaults to an empty range) and so do not
130
+ // enclose the name span; fall back to the name range to keep the invariant.
131
+ let contains = (range.start.line, range.start.character)
132
+ <= (selection_range.start.line, selection_range.start.character)
133
+ && (selection_range.end.line, selection_range.end.character)
134
+ <= (range.end.line, range.end.character);
135
+ let range = if contains { range } else { selection_range };
122
136
  DocumentSymbol {
123
137
  name,
124
138
  detail,
@@ -301,12 +315,35 @@ impl LanguageServer for Backend {
301
315
  .write()
302
316
  .unwrap()
303
317
  .insert(uri.clone(), chg.text.clone());
304
- publish_parse_and_lint(&self.client, uri, &chg.text).await;
318
+ // Bump this document's edit sequence and debounce the (expensive) analysis: only the
319
+ // task whose sequence is still current after the delay publishes, so a burst of
320
+ // keystrokes coalesces into one recompute instead of one-per-change.
321
+ let seq = {
322
+ let mut g = self.edit_seq.write().unwrap();
323
+ let n = g.entry(uri.clone()).or_insert(0);
324
+ *n += 1;
325
+ *n
326
+ };
327
+ let client = self.client.clone();
328
+ let docs = Arc::clone(&self.docs);
329
+ let edit_seq = Arc::clone(&self.edit_seq);
330
+ tokio::spawn(async move {
331
+ tokio::time::sleep(std::time::Duration::from_millis(200)).await;
332
+ // Superseded by a newer edit while we waited — drop this stale recompute.
333
+ if edit_seq.read().unwrap().get(&uri).copied() != Some(seq) {
334
+ return;
335
+ }
336
+ let text = docs.read().unwrap().get(&uri).cloned();
337
+ if let Some(text) = text {
338
+ publish_parse_and_lint(&client, uri, &text).await;
339
+ }
340
+ });
305
341
  }
306
342
  }
307
343
 
308
344
  async fn did_close(&self, p: DidCloseTextDocumentParams) {
309
345
  self.docs.write().unwrap().remove(&p.text_document.uri);
346
+ self.edit_seq.write().unwrap().remove(&p.text_document.uri);
310
347
  self.client
311
348
  .publish_diagnostics(p.text_document.uri, vec![], None)
312
349
  .await;
@@ -691,6 +728,30 @@ impl LanguageServer for Backend {
691
728
  let Ok(program) = tishlang_parser::parse(&text) else {
692
729
  return Ok(None);
693
730
  };
731
+ // Type-alias rename: the value resolver can't see `: T` annotation uses (type names live
732
+ // in a separate namespace), so handle a cursor on a type-alias declaration/use here,
733
+ // editing the declaration and every annotation site together.
734
+ if let Some(spans) =
735
+ tishlang_resolve::type_alias_rename_spans(&program, &text, pos.line, pos.character)
736
+ {
737
+ let mut edits: Vec<TextEdit> = spans
738
+ .into_iter()
739
+ .map(|sp| TextEdit {
740
+ range: span_to_range(&sp, &text),
741
+ new_text: new_name.clone(),
742
+ })
743
+ .collect();
744
+ edits.sort_by(|a, b| {
745
+ (b.range.start.line, b.range.start.character)
746
+ .cmp(&(a.range.start.line, a.range.start.character))
747
+ });
748
+ let mut m = HashMap::new();
749
+ m.insert(uri.clone(), edits);
750
+ return Ok(Some(WorkspaceEdit {
751
+ changes: Some(m),
752
+ ..Default::default()
753
+ }));
754
+ }
694
755
  let Some(def) = tishlang_resolve::definition_span(&program, &text, pos.line, pos.character)
695
756
  else {
696
757
  return Ok(None);
@@ -1065,7 +1126,7 @@ fn is_ident_char(c: char) -> bool {
1065
1126
  fn render_type(t: &tishlang_ast::TypeAnnotation) -> String {
1066
1127
  use tishlang_ast::{TypeAnnotation as T, TypeLiteral as L};
1067
1128
  match t {
1068
- T::Simple(s) => s.to_string(),
1129
+ T::Simple(s, _) => s.to_string(),
1069
1130
  T::Array(inner) => {
1070
1131
  // Parenthesize composite element types so `(A | B)[]` reads unambiguously.
1071
1132
  if matches!(
@@ -1119,7 +1180,7 @@ fn shallow_expr_type(e: &tishlang_ast::Expr) -> Option<tishlang_ast::TypeAnnotat
1119
1180
  Literal::Bool(_) => "boolean",
1120
1181
  Literal::Null => "null",
1121
1182
  };
1122
- Some(T::Simple(Arc::from(name)))
1183
+ Some(T::Simple(Arc::from(name), tishlang_ast::Span::default()))
1123
1184
  } else {
1124
1185
  None
1125
1186
  }
@@ -1652,11 +1713,11 @@ mod hover_tests {
1652
1713
  #[test]
1653
1714
  fn composite_types_render() {
1654
1715
  use tishlang_ast::{TypeAnnotation as T, TypeLiteral as L};
1655
- let arr = T::Array(Box::new(T::Simple("number".into())));
1716
+ let arr = T::Array(Box::new(T::Simple("number".into(), tishlang_ast::Span::default())));
1656
1717
  assert_eq!(render_type(&arr), "number[]");
1657
- let tup = T::Tuple(vec![T::Simple("number".into()), T::Simple("string".into())]);
1718
+ let tup = T::Tuple(vec![T::Simple("number".into(), tishlang_ast::Span::default()), T::Simple("string".into(), tishlang_ast::Span::default())]);
1658
1719
  assert_eq!(render_type(&tup), "[number, string]");
1659
- let uni = T::Union(vec![T::Simple("number".into()), T::Simple("null".into())]);
1720
+ let uni = T::Union(vec![T::Simple("number".into(), tishlang_ast::Span::default()), T::Simple("null".into(), tishlang_ast::Span::default())]);
1660
1721
  assert_eq!(render_type(&uni), "number | null");
1661
1722
  assert_eq!(render_type(&T::Literal(L::Str("on".into()))), "\"on\"");
1662
1723
  let arr_of_union = T::Array(Box::new(uni));
@@ -1671,6 +1732,50 @@ mod hover_tests {
1671
1732
  assert_eq!(full_doc_end(""), (0, 0));
1672
1733
  assert_eq!(full_doc_end("café"), (0, 4)); // UTF-16 units (é = 1), not bytes (5)
1673
1734
  }
1735
+
1736
+ #[test]
1737
+ fn doc_symbols_satisfy_lsp_selection_containment() {
1738
+ // LSP requires every DocumentSymbol's selectionRange ⊆ range, or VS Code rejects the
1739
+ // whole outline ("selectionRange must be contained in fullRange"). Exercise the
1740
+ // declaration forms the outline emits.
1741
+ use tower_lsp::lsp_types::DocumentSymbol;
1742
+ fn check(syms: &[DocumentSymbol], src: &str) {
1743
+ for s in syms {
1744
+ let (r, sel) = (&s.range, &s.selection_range);
1745
+ let contained = (r.start.line, r.start.character)
1746
+ <= (sel.start.line, sel.start.character)
1747
+ && (sel.end.line, sel.end.character) <= (r.end.line, r.end.character);
1748
+ assert!(
1749
+ contained,
1750
+ "selectionRange {sel:?} not contained in range {r:?} for `{}` in:\n{src}",
1751
+ s.name
1752
+ );
1753
+ if let Some(children) = &s.children {
1754
+ check(children, src);
1755
+ }
1756
+ }
1757
+ }
1758
+ let sources = [
1759
+ "fn f(x) { return x }\n",
1760
+ "let a = 1\n",
1761
+ "let a = 1, b = 2\n",
1762
+ "export fn g() { return 1 }\n",
1763
+ "export let x = 1\n",
1764
+ "type T = number\n",
1765
+ "declare fn h(): void\n",
1766
+ "declare let y: number\n",
1767
+ "fn outer() {\n fn inner() { return 1 }\n return inner\n}\n",
1768
+ "export type Opts = { a: number }\n",
1769
+ ];
1770
+ for src in sources {
1771
+ let p = parse(src);
1772
+ let mut syms = Vec::new();
1773
+ for s in &p.statements {
1774
+ doc_symbol_stmt(s, src, &mut syms);
1775
+ }
1776
+ check(&syms, src);
1777
+ }
1778
+ }
1674
1779
  }
1675
1780
 
1676
1781
  #[cfg(test)]
@@ -68,7 +68,7 @@ fn mangle_generic(base: &str, args: &[TypeAnnotation]) -> String {
68
68
 
69
69
  fn mangle_type(t: &TypeAnnotation) -> String {
70
70
  match t {
71
- TypeAnnotation::Simple(s) => s.chars().map(|c| if c.is_alphanumeric() { c } else { '_' }).collect(),
71
+ TypeAnnotation::Simple(s, _) => s.chars().map(|c| if c.is_alphanumeric() { c } else { '_' }).collect(),
72
72
  TypeAnnotation::Array(inner) => format!("{}Arr", mangle_type(inner)),
73
73
  TypeAnnotation::Tuple(es) => {
74
74
  format!("Tup{}", es.iter().map(mangle_type).collect::<Vec<_>>().join(""))
@@ -84,7 +84,7 @@ fn mangle_type(t: &TypeAnnotation) -> String {
84
84
  /// Substitute generic type parameters with their concrete arguments throughout a type body.
85
85
  fn subst_type(t: &TypeAnnotation, map: &HashMap<&str, &TypeAnnotation>) -> TypeAnnotation {
86
86
  match t {
87
- TypeAnnotation::Simple(s) => map
87
+ TypeAnnotation::Simple(s, _) => map
88
88
  .get(s.as_ref())
89
89
  .map(|rep| (*rep).clone())
90
90
  .unwrap_or_else(|| t.clone()),
@@ -758,7 +758,10 @@ impl<'a> Parser<'a> {
758
758
  }
759
759
  Some(TokenKind::Question) => {
760
760
  self.advance(); // ?
761
- t = TypeAnnotation::Union(vec![t, TypeAnnotation::Simple("null".into())]);
761
+ t = TypeAnnotation::Union(vec![
762
+ t,
763
+ TypeAnnotation::Simple("null".into(), Span::default()),
764
+ ]);
762
765
  }
763
766
  _ => break,
764
767
  }
@@ -772,6 +775,10 @@ impl<'a> Parser<'a> {
772
775
  Some(TokenKind::Ident) => {
773
776
  let tok = self.advance().ok_or("Expected type name")?;
774
777
  let name = tok.literal.clone().ok_or("Expected type name")?;
778
+ let span = Span {
779
+ start: tok.span.start,
780
+ end: tok.span.end,
781
+ };
775
782
  // Generic reference `Name<Args>`: `Array<T>` desugars to the native `T[]`; other
776
783
  // generic refs erase their args (the base name resolves to its alias, whose type
777
784
  // params already act as unknown -> `Value`).
@@ -786,25 +793,29 @@ impl<'a> Parser<'a> {
786
793
  // alias `Box__number` (a native struct). Falls back to erasing the args when
787
794
  // `name` isn't a known generic alias (e.g. forward reference) or arity mismatches.
788
795
  if let Some(spec) = self.monomorphize_generic(name.as_ref(), &args) {
789
- return Ok(TypeAnnotation::Simple(Arc::from(spec.as_str())));
796
+ return Ok(TypeAnnotation::Simple(Arc::from(spec.as_str()), span));
790
797
  }
791
- return Ok(TypeAnnotation::Simple(name));
798
+ return Ok(TypeAnnotation::Simple(name, span));
792
799
  }
793
- Ok(TypeAnnotation::Simple(name))
800
+ Ok(TypeAnnotation::Simple(name, span))
794
801
  }
795
802
  Some(TokenKind::Type | TokenKind::Declare) => {
796
803
  let tok = self.advance().ok_or("Expected type name")?;
797
804
  let name = tok.literal.clone().ok_or("Expected type name")?;
798
- Ok(TypeAnnotation::Simple(name))
805
+ let span = Span {
806
+ start: tok.span.start,
807
+ end: tok.span.end,
808
+ };
809
+ Ok(TypeAnnotation::Simple(name, span))
799
810
  }
800
811
  // Handle keywords that can be type names
801
812
  Some(TokenKind::Null) => {
802
813
  self.advance();
803
- Ok(TypeAnnotation::Simple("null".into()))
814
+ Ok(TypeAnnotation::Simple("null".into(), Span::default()))
804
815
  }
805
816
  Some(TokenKind::Void) => {
806
817
  self.advance();
807
- Ok(TypeAnnotation::Simple("void".into()))
818
+ Ok(TypeAnnotation::Simple("void".into(), Span::default()))
808
819
  }
809
820
  Some(TokenKind::LBrace) => {
810
821
  // Object type: { key: Type, ... }
@@ -3570,6 +3570,358 @@ pub fn shallow_module_bindings(program: &Program) -> Vec<(Arc<str>, tishlang_ast
3570
3570
  }
3571
3571
 
3572
3572
  /// All spans (definition + uses) that resolve to `def_span` for `name`.
3573
+ // ---- Type-alias rename (#131) ---------------------------------------------------------------
3574
+ // Type names live in a namespace separate from value bindings, so the value resolver
3575
+ // (`definition_span` / `reference_spans_for_def`) never sees `: T` annotation uses. These walkers
3576
+ // gather every type-name occurrence — the alias declaration plus each span-carrying
3577
+ // `TypeAnnotation::Simple` reference in any annotation — so renaming a type alias edits the
3578
+ // declaration and all its uses together (previously only the declaration was edited, silently
3579
+ // breaking the program).
3580
+
3581
+ struct TypeOcc {
3582
+ name: Arc<str>,
3583
+ span: tishlang_ast::Span,
3584
+ is_decl: bool,
3585
+ }
3586
+
3587
+ fn tref_ann(ann: &tishlang_ast::TypeAnnotation, out: &mut Vec<TypeOcc>) {
3588
+ use tishlang_ast::TypeAnnotation as T;
3589
+ match ann {
3590
+ T::Simple(name, span) => out.push(TypeOcc {
3591
+ name: name.clone(),
3592
+ span: *span,
3593
+ is_decl: false,
3594
+ }),
3595
+ T::Array(inner) => tref_ann(inner, out),
3596
+ T::Object(fields) => {
3597
+ for (_, t) in fields {
3598
+ tref_ann(t, out);
3599
+ }
3600
+ }
3601
+ T::Function { params, returns } => {
3602
+ for p in params {
3603
+ tref_ann(p, out);
3604
+ }
3605
+ tref_ann(returns, out);
3606
+ }
3607
+ T::Union(ts) | T::Tuple(ts) | T::Intersection(ts) => {
3608
+ for t in ts {
3609
+ tref_ann(t, out);
3610
+ }
3611
+ }
3612
+ T::Literal(_) => {}
3613
+ }
3614
+ }
3615
+
3616
+ fn tref_typed_param(tp: &TypedParam, out: &mut Vec<TypeOcc>) {
3617
+ if let Some(t) = &tp.type_ann {
3618
+ tref_ann(t, out);
3619
+ }
3620
+ if let Some(d) = &tp.default {
3621
+ tref_expr(d, out);
3622
+ }
3623
+ }
3624
+
3625
+ fn tref_param(p: &FunParam, out: &mut Vec<TypeOcc>) {
3626
+ match p {
3627
+ FunParam::Simple(tp) => tref_typed_param(tp, out),
3628
+ FunParam::Destructure {
3629
+ type_ann, default, ..
3630
+ } => {
3631
+ if let Some(t) = type_ann {
3632
+ tref_ann(t, out);
3633
+ }
3634
+ if let Some(d) = default {
3635
+ tref_expr(d, out);
3636
+ }
3637
+ }
3638
+ }
3639
+ }
3640
+
3641
+ fn tref_arrow_body(body: &ArrowBody, out: &mut Vec<TypeOcc>) {
3642
+ match body {
3643
+ ArrowBody::Expr(e) => tref_expr(e, out),
3644
+ ArrowBody::Block(s) => tref_stmt(s, out),
3645
+ }
3646
+ }
3647
+
3648
+ fn tref_expr(expr: &Expr, out: &mut Vec<TypeOcc>) {
3649
+ match expr {
3650
+ Expr::ArrowFunction { params, body, .. } => {
3651
+ for p in params {
3652
+ tref_param(p, out);
3653
+ }
3654
+ tref_arrow_body(body, out);
3655
+ }
3656
+ Expr::Binary { left, right, .. } | Expr::NullishCoalesce { left, right, .. } => {
3657
+ tref_expr(left, out);
3658
+ tref_expr(right, out);
3659
+ }
3660
+ Expr::Unary { operand, .. }
3661
+ | Expr::TypeOf { operand, .. }
3662
+ | Expr::Await { operand, .. } => tref_expr(operand, out),
3663
+ Expr::Delete { target, .. } => tref_expr(target, out),
3664
+ Expr::Call { callee, args, .. } | Expr::New { callee, args, .. } => {
3665
+ tref_expr(callee, out);
3666
+ for a in args {
3667
+ match a {
3668
+ CallArg::Expr(e) | CallArg::Spread(e) => tref_expr(e, out),
3669
+ }
3670
+ }
3671
+ }
3672
+ Expr::Member { object, .. } => tref_expr(object, out),
3673
+ Expr::Index { object, index, .. } => {
3674
+ tref_expr(object, out);
3675
+ tref_expr(index, out);
3676
+ }
3677
+ Expr::Conditional {
3678
+ cond,
3679
+ then_branch,
3680
+ else_branch,
3681
+ ..
3682
+ } => {
3683
+ tref_expr(cond, out);
3684
+ tref_expr(then_branch, out);
3685
+ tref_expr(else_branch, out);
3686
+ }
3687
+ Expr::Array { elements, .. } => {
3688
+ for el in elements {
3689
+ match el {
3690
+ tishlang_ast::ArrayElement::Expr(e)
3691
+ | tishlang_ast::ArrayElement::Spread(e) => tref_expr(e, out),
3692
+ }
3693
+ }
3694
+ }
3695
+ Expr::Object { props, .. } => {
3696
+ for p in props {
3697
+ match p {
3698
+ tishlang_ast::ObjectProp::KeyValue(_, e)
3699
+ | tishlang_ast::ObjectProp::Spread(e) => tref_expr(e, out),
3700
+ }
3701
+ }
3702
+ }
3703
+ Expr::Assign { value, .. }
3704
+ | Expr::CompoundAssign { value, .. }
3705
+ | Expr::LogicalAssign { value, .. } => tref_expr(value, out),
3706
+ Expr::MemberAssign { object, value, .. } => {
3707
+ tref_expr(object, out);
3708
+ tref_expr(value, out);
3709
+ }
3710
+ Expr::IndexAssign {
3711
+ object,
3712
+ index,
3713
+ value,
3714
+ ..
3715
+ } => {
3716
+ tref_expr(object, out);
3717
+ tref_expr(index, out);
3718
+ tref_expr(value, out);
3719
+ }
3720
+ Expr::TemplateLiteral { exprs, .. } => {
3721
+ for e in exprs {
3722
+ tref_expr(e, out);
3723
+ }
3724
+ }
3725
+ _ => {}
3726
+ }
3727
+ }
3728
+
3729
+ fn tref_stmt(stmt: &Statement, out: &mut Vec<TypeOcc>) {
3730
+ match stmt {
3731
+ Statement::TypeAlias {
3732
+ name,
3733
+ name_span,
3734
+ ty,
3735
+ ..
3736
+ } => {
3737
+ out.push(TypeOcc {
3738
+ name: name.clone(),
3739
+ span: *name_span,
3740
+ is_decl: true,
3741
+ });
3742
+ tref_ann(ty, out);
3743
+ }
3744
+ Statement::VarDecl { type_ann, init, .. } => {
3745
+ if let Some(t) = type_ann {
3746
+ tref_ann(t, out);
3747
+ }
3748
+ if let Some(e) = init {
3749
+ tref_expr(e, out);
3750
+ }
3751
+ }
3752
+ Statement::VarDeclDestructure { init, .. } => tref_expr(init, out),
3753
+ Statement::DeclareVar { type_ann, .. } => {
3754
+ if let Some(t) = type_ann {
3755
+ tref_ann(t, out);
3756
+ }
3757
+ }
3758
+ Statement::FunDecl {
3759
+ params,
3760
+ rest_param,
3761
+ return_type,
3762
+ body,
3763
+ ..
3764
+ } => {
3765
+ for p in params {
3766
+ tref_param(p, out);
3767
+ }
3768
+ if let Some(rp) = rest_param {
3769
+ tref_typed_param(rp, out);
3770
+ }
3771
+ if let Some(t) = return_type {
3772
+ tref_ann(t, out);
3773
+ }
3774
+ tref_stmt(body, out);
3775
+ }
3776
+ Statement::DeclareFun {
3777
+ params,
3778
+ rest_param,
3779
+ return_type,
3780
+ ..
3781
+ } => {
3782
+ for p in params {
3783
+ tref_param(p, out);
3784
+ }
3785
+ if let Some(rp) = rest_param {
3786
+ tref_typed_param(rp, out);
3787
+ }
3788
+ if let Some(t) = return_type {
3789
+ tref_ann(t, out);
3790
+ }
3791
+ }
3792
+ Statement::Block { statements, .. } | Statement::Multi { statements, .. } => {
3793
+ for s in statements {
3794
+ tref_stmt(s, out);
3795
+ }
3796
+ }
3797
+ Statement::ExprStmt { expr, .. } => tref_expr(expr, out),
3798
+ Statement::If {
3799
+ cond,
3800
+ then_branch,
3801
+ else_branch,
3802
+ ..
3803
+ } => {
3804
+ tref_expr(cond, out);
3805
+ tref_stmt(then_branch, out);
3806
+ if let Some(e) = else_branch {
3807
+ tref_stmt(e, out);
3808
+ }
3809
+ }
3810
+ Statement::While { cond, body, .. } => {
3811
+ tref_expr(cond, out);
3812
+ tref_stmt(body, out);
3813
+ }
3814
+ Statement::For {
3815
+ init,
3816
+ cond,
3817
+ update,
3818
+ body,
3819
+ ..
3820
+ } => {
3821
+ if let Some(i) = init {
3822
+ tref_stmt(i, out);
3823
+ }
3824
+ if let Some(c) = cond {
3825
+ tref_expr(c, out);
3826
+ }
3827
+ if let Some(u) = update {
3828
+ tref_expr(u, out);
3829
+ }
3830
+ tref_stmt(body, out);
3831
+ }
3832
+ Statement::ForOf { iterable, body, .. } => {
3833
+ tref_expr(iterable, out);
3834
+ tref_stmt(body, out);
3835
+ }
3836
+ Statement::DoWhile { body, cond, .. } => {
3837
+ tref_stmt(body, out);
3838
+ tref_expr(cond, out);
3839
+ }
3840
+ Statement::Return { value, .. } => {
3841
+ if let Some(e) = value {
3842
+ tref_expr(e, out);
3843
+ }
3844
+ }
3845
+ Statement::Throw { value, .. } => tref_expr(value, out),
3846
+ Statement::Switch {
3847
+ expr,
3848
+ cases,
3849
+ default_body,
3850
+ ..
3851
+ } => {
3852
+ tref_expr(expr, out);
3853
+ for (c, body) in cases {
3854
+ if let Some(c) = c {
3855
+ tref_expr(c, out);
3856
+ }
3857
+ for s in body {
3858
+ tref_stmt(s, out);
3859
+ }
3860
+ }
3861
+ if let Some(body) = default_body {
3862
+ for s in body {
3863
+ tref_stmt(s, out);
3864
+ }
3865
+ }
3866
+ }
3867
+ Statement::Try {
3868
+ body,
3869
+ catch_body,
3870
+ finally_body,
3871
+ ..
3872
+ } => {
3873
+ tref_stmt(body, out);
3874
+ if let Some(c) = catch_body {
3875
+ tref_stmt(c, out);
3876
+ }
3877
+ if let Some(f) = finally_body {
3878
+ tref_stmt(f, out);
3879
+ }
3880
+ }
3881
+ Statement::Export { declaration, .. } => match declaration.as_ref() {
3882
+ ExportDeclaration::Named(inner) => tref_stmt(inner, out),
3883
+ ExportDeclaration::Default(e) => tref_expr(e, out),
3884
+ },
3885
+ _ => {}
3886
+ }
3887
+ }
3888
+
3889
+ /// If the cursor sits on a type-alias declaration name or on a `: T` type-reference use, returns
3890
+ /// every source span that must be renamed together (the declaration plus all annotation uses).
3891
+ /// Returns `None` when the cursor is not on a user-declared type alias, so callers fall back to
3892
+ /// value-binding rename. Fixes the silent breakage where renaming a `type T` left every `: T`
3893
+ /// annotation stale (type names are in a namespace the value resolver does not see).
3894
+ pub fn type_alias_rename_spans(
3895
+ program: &Program,
3896
+ source: &str,
3897
+ lsp_line: u32,
3898
+ lsp_character: u32,
3899
+ ) -> Option<Vec<tishlang_ast::Span>> {
3900
+ let mut occ: Vec<TypeOcc> = Vec::new();
3901
+ for s in &program.statements {
3902
+ tref_stmt(s, &mut occ);
3903
+ }
3904
+ // The type name whose declaration or use span contains the cursor.
3905
+ let target = occ
3906
+ .iter()
3907
+ .find(|o| pos::span_contains_lsp_position(source, &o.span, lsp_line, lsp_character))?
3908
+ .name
3909
+ .clone();
3910
+ // Only rename user-declared aliases — never a builtin (`number`, `string`, …) used in an
3911
+ // annotation, which has no declaration to keep in sync.
3912
+ if !occ.iter().any(|o| o.is_decl && o.name == target) {
3913
+ return None;
3914
+ }
3915
+ let mut spans: Vec<tishlang_ast::Span> = occ
3916
+ .iter()
3917
+ .filter(|o| o.name == target)
3918
+ .map(|o| o.span)
3919
+ .collect();
3920
+ spans.sort_by_key(|s| (s.start.0, s.start.1, s.end.0, s.end.1));
3921
+ spans.dedup();
3922
+ Some(spans)
3923
+ }
3924
+
3573
3925
  pub fn reference_spans_for_def(
3574
3926
  program: &Program,
3575
3927
  source: &str,
@@ -4329,4 +4681,32 @@ mod tests {
4329
4681
  assert_eq!(ch.members[0].as_ref(), "b");
4330
4682
  assert_eq!(ch.members[1].as_ref(), "c");
4331
4683
  }
4684
+
4685
+ #[test]
4686
+ fn type_alias_rename_collects_decl_and_all_annotation_uses() {
4687
+ // #131: renaming a type alias must edit the declaration AND every `: T` annotation, or the
4688
+ // program silently breaks. Type names are in a namespace the value resolver doesn't see.
4689
+ let src = "type T = number\nlet a: T = 1\nfn f(x: T): T { return x }\n";
4690
+ let program = parse(src).expect("parse");
4691
+ fn span_text(src: &str, sp: &tishlang_ast::Span) -> String {
4692
+ let lines: Vec<&str> = src.lines().collect();
4693
+ let (sl, sc) = sp.start;
4694
+ let (_el, ec) = sp.end;
4695
+ lines[sl - 1].chars().skip(sc - 1).take(ec - sc).collect()
4696
+ }
4697
+ // Cursor on the declaration name `T` (0-indexed line 0, char 5).
4698
+ let spans = type_alias_rename_spans(&program, src, 0, 5).expect("type rename spans");
4699
+ assert_eq!(spans.len(), 4, "decl + let-ann + param-ann + return-ann; spans={spans:?}");
4700
+ for sp in &spans {
4701
+ assert_eq!(span_text(src, sp), "T", "every rename span covers the `T` token; {sp:?}");
4702
+ }
4703
+ // Initiating from a `: T` use also works (the param annotation, line 2 char 8).
4704
+ let from_use = type_alias_rename_spans(&program, src, 2, 8).expect("rename from use");
4705
+ assert_eq!(from_use.len(), 4, "same set whether initiated from decl or use");
4706
+ // A builtin used in an annotation (`number`, line 0 char 9) is not a user alias -> None.
4707
+ assert!(
4708
+ type_alias_rename_spans(&program, src, 0, 9).is_none(),
4709
+ "builtin `number` has no declaration to rename"
4710
+ );
4711
+ }
4332
4712
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tishlang/tish",
3
- "version": "2.2.4",
3
+ "version": "2.2.7",
4
4
  "description": "Tish - minimal TS/JS-compatible language. Run, REPL, build to native or other targets.",
5
5
  "license": "PIF",
6
6
  "repository": {
Binary file
Binary file
Binary file
Binary file
Binary file