jaclang 0.7.0__py3-none-any.whl → 0.7.2__py3-none-any.whl

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.

Potentially problematic release.


This version of jaclang might be problematic. Click here for more details.

Files changed (62) hide show
  1. jaclang/compiler/absyntree.py +53 -50
  2. jaclang/compiler/compile.py +21 -0
  3. jaclang/compiler/passes/main/__init__.py +2 -2
  4. jaclang/compiler/passes/main/def_impl_match_pass.py +10 -8
  5. jaclang/compiler/passes/main/def_use_pass.py +14 -7
  6. jaclang/compiler/passes/main/fuse_typeinfo_pass.py +20 -1
  7. jaclang/compiler/passes/main/import_pass.py +60 -11
  8. jaclang/compiler/passes/main/pyast_gen_pass.py +65 -4
  9. jaclang/compiler/passes/main/pyast_load_pass.py +2 -1
  10. jaclang/compiler/passes/main/pyjac_ast_link_pass.py +6 -1
  11. jaclang/compiler/passes/main/pyout_pass.py +3 -1
  12. jaclang/compiler/passes/main/schedules.py +4 -3
  13. jaclang/compiler/passes/main/tests/fixtures/incautoimpl.jac +7 -0
  14. jaclang/compiler/passes/main/tests/test_decl_def_match_pass.py +4 -4
  15. jaclang/compiler/passes/main/tests/test_import_pass.py +21 -0
  16. jaclang/compiler/passes/main/tests/test_type_check_pass.py +1 -1
  17. jaclang/compiler/passes/tool/jac_formatter_pass.py +14 -2
  18. jaclang/compiler/passes/tool/tests/fixtures/doc_string.jac +15 -0
  19. jaclang/compiler/passes/tool/tests/test_jac_format_pass.py +7 -5
  20. jaclang/compiler/passes/tool/tests/test_unparse_validate.py +1 -2
  21. jaclang/compiler/symtable.py +21 -1
  22. jaclang/core/aott.py +107 -11
  23. jaclang/core/construct.py +171 -5
  24. jaclang/core/llms/anthropic.py +31 -2
  25. jaclang/core/llms/base.py +3 -3
  26. jaclang/core/llms/groq.py +4 -1
  27. jaclang/core/llms/huggingface.py +4 -1
  28. jaclang/core/llms/ollama.py +4 -1
  29. jaclang/core/llms/openai.py +6 -2
  30. jaclang/core/llms/togetherai.py +4 -1
  31. jaclang/langserve/engine.py +193 -121
  32. jaclang/langserve/server.py +35 -6
  33. jaclang/langserve/tests/fixtures/circle.jac +73 -0
  34. jaclang/langserve/tests/fixtures/circle_err.jac +73 -0
  35. jaclang/langserve/tests/fixtures/circle_pure.impl.jac +32 -0
  36. jaclang/langserve/tests/fixtures/circle_pure.jac +34 -0
  37. jaclang/langserve/tests/fixtures/circle_pure_err.impl.jac +32 -0
  38. jaclang/langserve/tests/fixtures/circle_pure_err.jac +34 -0
  39. jaclang/langserve/tests/test_server.py +156 -1
  40. jaclang/langserve/utils.py +127 -2
  41. jaclang/plugin/default.py +25 -83
  42. jaclang/plugin/feature.py +10 -12
  43. jaclang/plugin/tests/test_features.py +0 -33
  44. jaclang/settings.py +1 -0
  45. jaclang/tests/fixtures/byllmissue.jac +3 -0
  46. jaclang/tests/fixtures/hash_init_check.jac +17 -0
  47. jaclang/tests/fixtures/math_question.jpg +0 -0
  48. jaclang/tests/fixtures/nosigself.jac +19 -0
  49. jaclang/tests/fixtures/type_info.jac +1 -1
  50. jaclang/tests/fixtures/walker_override.jac +21 -0
  51. jaclang/tests/fixtures/with_llm_vision.jac +25 -0
  52. jaclang/tests/test_cli.py +1 -1
  53. jaclang/tests/test_language.py +61 -11
  54. jaclang/utils/helpers.py +3 -5
  55. jaclang/utils/test.py +1 -1
  56. jaclang/utils/treeprinter.py +19 -2
  57. {jaclang-0.7.0.dist-info → jaclang-0.7.2.dist-info}/METADATA +3 -2
  58. {jaclang-0.7.0.dist-info → jaclang-0.7.2.dist-info}/RECORD +60 -48
  59. jaclang/core/memory.py +0 -48
  60. jaclang/core/shelve_storage.py +0 -55
  61. {jaclang-0.7.0.dist-info → jaclang-0.7.2.dist-info}/WHEEL +0 -0
  62. {jaclang-0.7.0.dist-info → jaclang-0.7.2.dist-info}/entry_points.txt +0 -0
@@ -45,12 +45,41 @@ class Anthropic(BaseLLM):
45
45
  self.client = anthropic.Anthropic()
46
46
  self.verbose = verbose
47
47
  self.max_tries = max_tries
48
- self.model_name = kwargs.get("model_name", "claude-3-sonnet-20240229")
48
+ self.model_name = str(kwargs.get("model_name", "claude-3-sonnet-20240229"))
49
49
  self.temperature = kwargs.get("temperature", 0.7)
50
50
  self.max_tokens = kwargs.get("max_tokens", 1024)
51
51
 
52
- def __infer__(self, meaning_in: str, **kwargs: dict) -> str:
52
+ def __infer__(self, meaning_in: str | list[dict], **kwargs: dict) -> str:
53
53
  """Infer a response from the input meaning."""
54
+ if not isinstance(meaning_in, str):
55
+ assert self.model_name.startswith(
56
+ ("claude-3-opus", "claude-3-sonnet", "claude-3-haiku")
57
+ ), f"Model {self.model_name} is not multimodal, use a multimodal model instead."
58
+
59
+ import re
60
+
61
+ formatted_meaning_in = []
62
+ for item in meaning_in:
63
+ if item["type"] == "image_url":
64
+ # "data:image/jpeg;base64,base64_string"
65
+ img_match = re.match(
66
+ r"data:(image/[a-zA-Z]*);base64,(.*)", item["source"]
67
+ )
68
+ if img_match:
69
+ media_type, base64_string = img_match.groups()
70
+ formatted_meaning_in.append(
71
+ {
72
+ "type": "image",
73
+ "source": {
74
+ "type": "base64",
75
+ "media_type": media_type,
76
+ "data": base64_string,
77
+ },
78
+ }
79
+ )
80
+ continue
81
+ formatted_meaning_in.append(item)
82
+ meaning_in = formatted_meaning_in
54
83
  messages = [{"role": "user", "content": meaning_in}]
55
84
  output = self.client.messages.create(
56
85
  model=kwargs.get("model_name", self.model_name),
jaclang/core/llms/base.py CHANGED
@@ -112,11 +112,11 @@ class BaseLLM:
112
112
  self.max_tries = max_tries
113
113
  raise NotImplementedError
114
114
 
115
- def __infer__(self, meaning_in: str, **kwargs: dict) -> str:
115
+ def __infer__(self, meaning_in: str | list[dict], **kwargs: dict) -> str:
116
116
  """Infer a response from the input meaning."""
117
117
  raise NotImplementedError
118
118
 
119
- def __call__(self, input_text: str, **kwargs: dict) -> str:
119
+ def __call__(self, input_text: str | list[dict], **kwargs: dict) -> str:
120
120
  """Infer a response from the input text."""
121
121
  if self.verbose:
122
122
  logger.info(f"Meaning In\n{input_text}")
@@ -131,7 +131,7 @@ class BaseLLM:
131
131
  ) -> str:
132
132
  """Resolve the output string to return the reasoning and output."""
133
133
  if self.verbose:
134
- logger.opt(colors=True).info(f"Meaning Out\n<green>{meaning_out}</green>")
134
+ logger.info(f"Meaning Out\n{meaning_out}")
135
135
  output_match = re.search(r"\[Output\](.*)", meaning_out)
136
136
  output = output_match.group(1).strip() if output_match else None
137
137
  if not output_match:
jaclang/core/llms/groq.py CHANGED
@@ -49,8 +49,11 @@ class Groq(BaseLLM):
49
49
  self.temperature = kwargs.get("temperature", 0.7)
50
50
  self.max_tokens = kwargs.get("max_tokens", 1024)
51
51
 
52
- def __infer__(self, meaning_in: str, **kwargs: dict) -> str:
52
+ def __infer__(self, meaning_in: str | list[dict], **kwargs: dict) -> str:
53
53
  """Infer a response from the input meaning."""
54
+ assert isinstance(
55
+ meaning_in, str
56
+ ), "Currently Multimodal models are not supported. Please provide a string input."
54
57
  messages = [{"role": "user", "content": meaning_in}]
55
58
  model_params = {
56
59
  k: v
@@ -61,8 +61,11 @@ class Huggingface(BaseLLM):
61
61
  self.temperature = kwargs.get("temperature", 0.7)
62
62
  self.max_tokens = kwargs.get("max_new_tokens", 1024)
63
63
 
64
- def __infer__(self, meaning_in: str, **kwargs: dict) -> str:
64
+ def __infer__(self, meaning_in: str | list[dict], **kwargs: dict) -> str:
65
65
  """Infer a response from the input meaning."""
66
+ assert isinstance(
67
+ meaning_in, str
68
+ ), "Currently Multimodal models are not supported. Please provide a string input."
66
69
  messages = [{"role": "user", "content": meaning_in}]
67
70
  output = self.pipe(
68
71
  messages,
@@ -51,8 +51,11 @@ class Ollama(BaseLLM):
51
51
  k: v for k, v in kwargs.items() if k not in ["model_name", "host"]
52
52
  }
53
53
 
54
- def __infer__(self, meaning_in: str, **kwargs: dict) -> str:
54
+ def __infer__(self, meaning_in: str | list[dict], **kwargs: dict) -> str:
55
55
  """Infer a response from the input meaning."""
56
+ assert isinstance(
57
+ meaning_in, str
58
+ ), "Currently Multimodal models are not supported. Please provide a string input."
56
59
  model = str(kwargs.get("model_name", self.model_name))
57
60
  if not self.check_model(model):
58
61
  self.download_model(model)
@@ -45,12 +45,16 @@ class OpenAI(BaseLLM):
45
45
  self.client = openai.OpenAI()
46
46
  self.verbose = verbose
47
47
  self.max_tries = max_tries
48
- self.model_name = kwargs.get("model_name", "gpt-3.5-turbo")
48
+ self.model_name = str(kwargs.get("model_name", "gpt-3.5-turbo"))
49
49
  self.temperature = kwargs.get("temperature", 0.7)
50
50
  self.max_tokens = kwargs.get("max_tokens", 1024)
51
51
 
52
- def __infer__(self, meaning_in: str, **kwargs: dict) -> str:
52
+ def __infer__(self, meaning_in: str | list[dict], **kwargs: dict) -> str:
53
53
  """Infer a response from the input meaning."""
54
+ if not isinstance(meaning_in, str):
55
+ assert self.model_name.startswith(
56
+ ("gpt-4o", "gpt-4-turbo")
57
+ ), f"Model {self.model_name} is not multimodal, use a multimodal model instead."
54
58
  messages = [{"role": "user", "content": meaning_in}]
55
59
  output = self.client.chat.completions.create(
56
60
  model=kwargs.get("model_name", self.model_name),
@@ -48,8 +48,11 @@ class TogetherAI(BaseLLM):
48
48
  self.temperature = kwargs.get("temperature", 0.7)
49
49
  self.max_tokens = kwargs.get("max_tokens", 1024)
50
50
 
51
- def __infer__(self, meaning_in: str, **kwargs: dict) -> str:
51
+ def __infer__(self, meaning_in: str | list[dict], **kwargs: dict) -> str:
52
52
  """Infer a response from the input meaning."""
53
+ assert isinstance(
54
+ meaning_in, str
55
+ ), "Currently Multimodal models are not supported. Please provide a string input."
53
56
  messages = [{"role": "user", "content": meaning_in}]
54
57
  output = self.client.chat.completions.create(
55
58
  model=kwargs.get("model_name", self.model_name),
@@ -2,74 +2,79 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import logging
6
+ from enum import IntEnum
5
7
  from hashlib import md5
6
- from typing import Sequence, Type
8
+ from typing import Optional, Sequence
7
9
 
8
10
 
9
11
  import jaclang.compiler.absyntree as ast
10
- from jaclang.compiler.compile import jac_pass_to_pass, jac_str_to_pass
12
+ from jaclang.compiler.compile import jac_ir_to_pass, jac_str_to_pass
11
13
  from jaclang.compiler.parser import JacParser
12
14
  from jaclang.compiler.passes import Pass
13
- from jaclang.compiler.passes.main.schedules import (
14
- AccessCheckPass,
15
- PyBytecodeGenPass,
16
- py_code_gen_typed,
17
- )
15
+ from jaclang.compiler.passes.main.schedules import type_checker_sched
18
16
  from jaclang.compiler.passes.tool import FuseCommentsPass, JacFormatPass
19
17
  from jaclang.compiler.passes.transform import Alert
20
- from jaclang.compiler.symtable import Symbol
21
- from jaclang.langserve.utils import sym_tab_list
18
+ from jaclang.langserve.utils import (
19
+ collect_symbols,
20
+ create_range,
21
+ find_deepest_symbol_node_at_pos,
22
+ )
23
+ from jaclang.vendor.pygls import uris
22
24
  from jaclang.vendor.pygls.server import LanguageServer
23
- from jaclang.vendor.pygls.workspace.text_document import TextDocument
24
25
 
25
26
  import lsprotocol.types as lspt
26
27
 
27
28
 
29
+ class ALev(IntEnum):
30
+ """Analysis Level."""
31
+
32
+ QUICK = 1
33
+ DEEP = 2
34
+ TYPE = 3
35
+
36
+
28
37
  class ModuleInfo:
29
38
  """Module IR and Stats."""
30
39
 
31
40
  def __init__(
32
41
  self,
33
42
  ir: ast.Module,
34
- to_pass: Pass,
35
43
  errors: Sequence[Alert],
36
44
  warnings: Sequence[Alert],
45
+ alev: ALev,
46
+ parent: Optional[ModuleInfo] = None,
37
47
  ) -> None:
38
48
  """Initialize module info."""
39
49
  self.ir = ir
40
- self.at_pass = to_pass
41
50
  self.errors = errors
42
51
  self.warnings = warnings
52
+ self.alev = alev
53
+ self.parent: Optional[ModuleInfo] = parent
43
54
  self.diagnostics = self.gen_diagnostics()
44
55
 
56
+ @property
57
+ def uri(self) -> str:
58
+ """Return uri."""
59
+ return uris.from_fs_path(self.ir.loc.mod_path)
60
+
61
+ @property
62
+ def has_syntax_error(self) -> bool:
63
+ """Return if there are syntax errors."""
64
+ return len(self.errors) > 0 and self.alev == ALev.QUICK
65
+
45
66
  def gen_diagnostics(self) -> list[lspt.Diagnostic]:
46
67
  """Return diagnostics."""
47
68
  return [
48
69
  lspt.Diagnostic(
49
- range=lspt.Range(
50
- start=lspt.Position(
51
- line=error.loc.first_line, character=error.loc.col_start
52
- ),
53
- end=lspt.Position(
54
- line=error.loc.last_line,
55
- character=error.loc.col_end,
56
- ),
57
- ),
70
+ range=create_range(error.loc),
58
71
  message=error.msg,
59
72
  severity=lspt.DiagnosticSeverity.Error,
60
73
  )
61
74
  for error in self.errors
62
75
  ] + [
63
76
  lspt.Diagnostic(
64
- range=lspt.Range(
65
- start=lspt.Position(
66
- line=warning.loc.first_line, character=warning.loc.col_start
67
- ),
68
- end=lspt.Position(
69
- line=warning.loc.last_line,
70
- character=warning.loc.col_end,
71
- ),
72
- ),
77
+ range=create_range(warning.loc),
73
78
  message=warning.msg,
74
79
  severity=lspt.DiagnosticSeverity.Warning,
75
80
  )
@@ -85,18 +90,17 @@ class JacLangServer(LanguageServer):
85
90
  super().__init__("jac-lsp", "v0.1")
86
91
  self.modules: dict[str, ModuleInfo] = {}
87
92
 
88
- def module_not_diff(self, doc: TextDocument) -> bool:
93
+ def module_not_diff(self, uri: str, alev: ALev) -> bool:
89
94
  """Check if module was changed."""
95
+ doc = self.workspace.get_text_document(uri)
90
96
  return (
91
97
  doc.uri in self.modules
92
98
  and self.modules[doc.uri].ir.source.hash
93
99
  == md5(doc.source.encode()).hexdigest()
94
- )
95
-
96
- def module_reached_pass(self, doc: TextDocument, target: Type[Pass]) -> bool:
97
- """Check if module reached a pass."""
98
- return doc.uri in self.modules and isinstance(
99
- self.modules[doc.uri].at_pass, target
100
+ and (
101
+ self.modules[doc.uri].alev >= alev
102
+ or self.modules[doc.uri].has_syntax_error
103
+ )
100
104
  )
101
105
 
102
106
  def push_diagnostics(self, file_path: str) -> None:
@@ -107,73 +111,101 @@ class JacLangServer(LanguageServer):
107
111
  self.modules[file_path].diagnostics,
108
112
  )
109
113
 
110
- def quick_check(self, file_path: str) -> None:
114
+ def unwind_to_parent(self, file_path: str) -> str:
115
+ """Unwind to parent."""
116
+ orig_file_path = file_path
117
+ if file_path in self.modules:
118
+ while cur := self.modules[file_path].parent:
119
+ file_path = cur.uri
120
+ if file_path == orig_file_path and (
121
+ discover := self.modules[file_path].ir.annexable_by
122
+ ):
123
+ file_path = uris.from_fs_path(discover)
124
+ self.quick_check(file_path)
125
+ return file_path
126
+
127
+ def update_modules(self, file_path: str, build: Pass, alev: ALev) -> None:
128
+ """Update modules."""
129
+ if not isinstance(build.ir, ast.Module):
130
+ self.log_error("Error with module build.")
131
+ return
132
+ save_parent = (
133
+ self.modules[file_path].parent if file_path in self.modules else None
134
+ )
135
+ self.modules[file_path] = ModuleInfo(
136
+ ir=build.ir,
137
+ errors=[
138
+ i
139
+ for i in build.errors_had
140
+ if i.loc.mod_path == uris.to_fs_path(file_path)
141
+ ],
142
+ warnings=[
143
+ i
144
+ for i in build.warnings_had
145
+ if i.loc.mod_path == uris.to_fs_path(file_path)
146
+ ],
147
+ alev=alev,
148
+ )
149
+ self.modules[file_path].parent = save_parent
150
+ for p in build.ir.mod_deps.keys():
151
+ uri = uris.from_fs_path(p)
152
+ self.modules[uri] = ModuleInfo(
153
+ ir=build.ir.mod_deps[p],
154
+ errors=[i for i in build.errors_had if i.loc.mod_path == p],
155
+ warnings=[i for i in build.warnings_had if i.loc.mod_path == p],
156
+ alev=alev,
157
+ )
158
+ self.modules[uri].parent = (
159
+ self.modules[file_path] if file_path != uri else None
160
+ )
161
+
162
+ def quick_check(self, file_path: str, force: bool = False) -> None:
111
163
  """Rebuild a file."""
112
- document = self.workspace.get_document(file_path)
113
- if self.module_not_diff(document):
164
+ if not force and self.module_not_diff(file_path, ALev.QUICK):
114
165
  return
115
166
  try:
167
+ document = self.workspace.get_text_document(file_path)
116
168
  build = jac_str_to_pass(
117
169
  jac_str=document.source, file_path=document.path, schedule=[]
118
170
  )
119
171
  except Exception as e:
120
172
  self.log_error(f"Error during syntax check: {e}")
121
- if isinstance(build.ir, ast.Module):
122
- self.modules[file_path] = ModuleInfo(
123
- ir=build.ir,
124
- to_pass=build,
125
- errors=build.errors_had,
126
- warnings=build.warnings_had,
127
- )
173
+ self.update_modules(file_path, build, ALev.QUICK)
128
174
 
129
- def deep_check(self, file_path: str) -> None:
175
+ def deep_check(self, file_path: str, force: bool = False) -> None:
130
176
  """Rebuild a file and its dependencies."""
131
- document = self.workspace.get_document(file_path)
132
- if self.module_not_diff(document) and self.module_reached_pass(
133
- document, PyBytecodeGenPass
134
- ):
177
+ if file_path in self.modules:
178
+ self.quick_check(file_path, force=force)
179
+ if not force and self.module_not_diff(file_path, ALev.DEEP):
135
180
  return
136
181
  try:
137
- build = jac_pass_to_pass(in_pass=self.modules[file_path].at_pass)
182
+ file_path = self.unwind_to_parent(file_path)
183
+ build = jac_ir_to_pass(ir=self.modules[file_path].ir)
138
184
  except Exception as e:
139
185
  self.log_error(f"Error during syntax check: {e}")
140
- if isinstance(build.ir, ast.Module):
141
- self.modules[file_path] = ModuleInfo(
142
- ir=build.ir,
143
- to_pass=build,
144
- errors=build.errors_had,
145
- warnings=build.warnings_had,
146
- )
186
+ self.update_modules(file_path, build, ALev.DEEP)
147
187
 
148
- def type_check(self, file_path: str) -> None:
188
+ def type_check(self, file_path: str, force: bool = False) -> None:
149
189
  """Rebuild a file and its dependencies."""
150
- document = self.workspace.get_document(file_path)
151
- if self.module_not_diff(document) and self.module_reached_pass(
152
- document, AccessCheckPass
153
- ):
190
+ if file_path not in self.modules:
191
+ self.deep_check(file_path, force=force)
192
+ if not force and self.module_not_diff(file_path, ALev.TYPE):
154
193
  return
155
194
  try:
156
- build = jac_pass_to_pass(
157
- in_pass=self.modules[file_path].at_pass,
158
- target=AccessCheckPass,
159
- schedule=py_code_gen_typed,
195
+ file_path = self.unwind_to_parent(file_path)
196
+ build = jac_ir_to_pass(
197
+ ir=self.modules[file_path].ir, schedule=type_checker_sched
160
198
  )
161
199
  except Exception as e:
162
200
  self.log_error(f"Error during type check: {e}")
163
- if isinstance(build.ir, ast.Module):
164
- self.modules[file_path] = ModuleInfo(
165
- ir=build.ir,
166
- to_pass=build,
167
- errors=build.errors_had,
168
- warnings=build.warnings_had,
169
- )
201
+ self.update_modules(file_path, build, ALev.TYPE)
170
202
 
171
203
  def get_completion(
172
204
  self, file_path: str, position: lspt.Position
173
205
  ) -> lspt.CompletionList:
174
206
  """Return completion for a file."""
175
207
  items = []
176
- document = self.workspace.get_document(file_path)
208
+ document = self.workspace.get_text_document(file_path)
177
209
  current_line = document.lines[position.line].strip()
178
210
  if current_line.endswith("hello."):
179
211
 
@@ -197,7 +229,7 @@ class JacLangServer(LanguageServer):
197
229
  def formatted_jac(self, file_path: str) -> list[lspt.TextEdit]:
198
230
  """Return formatted jac."""
199
231
  try:
200
- document = self.workspace.get_document(file_path)
232
+ document = self.workspace.get_text_document(file_path)
201
233
  format = jac_str_to_pass(
202
234
  jac_str=document.source,
203
235
  file_path=document.path,
@@ -224,52 +256,88 @@ class JacLangServer(LanguageServer):
224
256
  )
225
257
  ]
226
258
 
227
- def get_dependencies(
228
- self, file_path: str, deep: bool = False
229
- ) -> list[ast.ModulePath]:
230
- """Return a list of dependencies for a file."""
231
- mod_ir = self.modules[file_path].ir
232
- if deep:
233
- return (
234
- [
235
- i
236
- for i in mod_ir.get_all_sub_nodes(ast.ModulePath)
237
- if i.parent_of_type(ast.Import).hint.tag.value == "jac"
238
- ]
239
- if mod_ir
240
- else []
259
+ def get_hover_info(
260
+ self, file_path: str, position: lspt.Position
261
+ ) -> Optional[lspt.Hover]:
262
+ """Return hover information for a file."""
263
+ node_selected = find_deepest_symbol_node_at_pos(
264
+ self.modules[file_path].ir, position.line, position.character
265
+ )
266
+ value = self.get_node_info(node_selected) if node_selected else None
267
+ if value:
268
+ return lspt.Hover(
269
+ contents=lspt.MarkupContent(
270
+ kind=lspt.MarkupKind.PlainText, value=f"{value}"
271
+ ),
241
272
  )
242
- else:
243
- return (
244
- [
245
- i
246
- for i in mod_ir.get_all_sub_nodes(ast.ModulePath)
247
- if i.loc.mod_path == file_path
248
- and i.parent_of_type(ast.Import).hint.tag.value == "jac"
249
- ]
250
- if mod_ir
251
- else []
273
+ return None
274
+
275
+ def get_node_info(self, node: ast.AstSymbolNode) -> Optional[str]:
276
+ """Extract meaningful information from the AST node."""
277
+ try:
278
+ if isinstance(node, ast.NameSpec):
279
+ node = node.name_of
280
+ access = node.sym_link.access.value + " " if node.sym_link else None
281
+ node_info = (
282
+ f"({access if access else ''}{node.sym_type.value}) {node.sym_name}"
252
283
  )
284
+ if node.sym_info.clean_type:
285
+ node_info += f": {node.sym_info.clean_type}"
286
+ if isinstance(node, ast.AstSemStrNode) and node.semstr:
287
+ node_info += f"\n{node.semstr.value}"
288
+ if isinstance(node, ast.AstDocNode) and node.doc:
289
+ node_info += f"\n{node.doc.value}"
290
+ if isinstance(node, ast.Ability) and node.signature:
291
+ node_info += f"\n{node.signature.unparse()}"
292
+ self.log_py(node.pp())
293
+ self.log_py(f"mypy_node: {node.gen.mypy_ast}")
294
+ except AttributeError as e:
295
+ self.log_warning(f"Attribute error when accessing node attributes: {e}")
296
+ return node_info.strip()
253
297
 
254
- def get_symbols(self, file_path: str) -> list[Symbol]:
255
- """Return a list of symbols for a file."""
256
- symbols = []
257
- mod_ir = self.modules[file_path].ir
258
- if file_path in self.modules:
259
- root_table = mod_ir.sym_tab if mod_ir else None
260
- if file_path in self.modules and root_table:
261
- for i in sym_tab_list(sym_tab=root_table, file_path=file_path):
262
- symbols += list(i.tab.values())
263
- return symbols
264
-
265
- def get_definitions(
266
- self, file_path: str
267
- ) -> Sequence[ast.AstSymbolNode]: # need test
268
- """Return a list of definitions for a file."""
269
- defs = []
270
- for i in self.get_symbols(file_path):
271
- defs += i.defn
272
- return defs
298
+ def get_document_symbols(self, file_path: str) -> list[lspt.DocumentSymbol]:
299
+ """Return document symbols for a file."""
300
+ root_node = self.modules[file_path].ir.sym_tab
301
+ if root_node:
302
+ return collect_symbols(root_node)
303
+ return []
304
+
305
+ def get_definition(
306
+ self, file_path: str, position: lspt.Position
307
+ ) -> Optional[lspt.Location]:
308
+ """Return definition location for a file."""
309
+ node_selected: Optional[ast.AstSymbolNode] = find_deepest_symbol_node_at_pos(
310
+ self.modules[file_path].ir, position.line, position.character
311
+ )
312
+ if node_selected:
313
+ if isinstance(node_selected, (ast.ElementStmt, ast.BuiltinType)):
314
+ return None
315
+ decl_node = (
316
+ node_selected.parent.body.target
317
+ if node_selected.parent
318
+ and isinstance(node_selected.parent, ast.AstImplNeedingNode)
319
+ and isinstance(node_selected.parent.body, ast.AstImplOnlyNode)
320
+ else (
321
+ node_selected.sym_link.decl
322
+ if (node_selected.sym_link and node_selected.sym_link.decl)
323
+ else node_selected
324
+ )
325
+ )
326
+ self.log_py(f"{node_selected}, {decl_node}")
327
+ decl_uri = uris.from_fs_path(decl_node.loc.mod_path)
328
+ try:
329
+ decl_range = create_range(decl_node.loc)
330
+ except ValueError: # 'print' name has decl in 0,0,0,0
331
+ return None
332
+ decl_location = lspt.Location(
333
+ uri=decl_uri,
334
+ range=decl_range,
335
+ )
336
+
337
+ return decl_location
338
+ else:
339
+ self.log_info("No declaration found for the selected node.")
340
+ return None
273
341
 
274
342
  def log_error(self, message: str) -> None:
275
343
  """Log an error message."""
@@ -285,3 +353,7 @@ class JacLangServer(LanguageServer):
285
353
  """Log an info message."""
286
354
  self.show_message_log(message, lspt.MessageType.Info)
287
355
  self.show_message(message, lspt.MessageType.Info)
356
+
357
+ def log_py(self, message: str) -> None:
358
+ """Log a message."""
359
+ logging.info(message)
@@ -15,21 +15,22 @@ analysis_thread: Optional[threading.Thread] = None
15
15
  analysis_stop_event = threading.Event()
16
16
 
17
17
 
18
- def analyze_and_publish(ls: JacLangServer, uri: str) -> None:
18
+ def analyze_and_publish(ls: JacLangServer, uri: str, level: int = 2) -> None:
19
19
  """Analyze and publish diagnostics."""
20
20
  global analysis_thread, analysis_stop_event
21
21
 
22
22
  def run_analysis() -> None:
23
23
  ls.quick_check(uri)
24
24
  ls.push_diagnostics(uri)
25
- ls.deep_check(uri)
26
- ls.push_diagnostics(uri)
27
- ls.type_check(uri)
28
- ls.push_diagnostics(uri)
25
+ if not analysis_stop_event.is_set() and level > 0:
26
+ ls.deep_check(uri)
27
+ ls.push_diagnostics(uri)
28
+ if not analysis_stop_event.is_set() and level > 1:
29
+ ls.type_check(uri)
30
+ ls.push_diagnostics(uri)
29
31
 
30
32
  analysis_thread = threading.Thread(target=run_analysis)
31
33
  analysis_thread.start()
32
- ls.log_info("Analysis restarted")
33
34
 
34
35
 
35
36
  def stop_analysis() -> None:
@@ -130,6 +131,34 @@ def formatting(
130
131
  return ls.formatted_jac(params.text_document.uri)
131
132
 
132
133
 
134
+ @server.feature(lspt.TEXT_DOCUMENT_HOVER, lspt.HoverOptions(work_done_progress=True))
135
+ def hover(
136
+ ls: JacLangServer, params: lspt.TextDocumentPositionParams
137
+ ) -> Optional[lspt.Hover]:
138
+ """Provide hover information for the given hover request."""
139
+ return ls.get_hover_info(params.text_document.uri, params.position)
140
+
141
+
142
+ @server.feature(lspt.TEXT_DOCUMENT_DOCUMENT_SYMBOL)
143
+ async def document_symbol(
144
+ ls: JacLangServer, params: lspt.DocumentSymbolParams
145
+ ) -> list[lspt.DocumentSymbol]:
146
+ """Provide document symbols."""
147
+ stop_analysis()
148
+ analyze_and_publish(ls, params.text_document.uri)
149
+ return ls.get_document_symbols(params.text_document.uri)
150
+
151
+
152
+ @server.feature(lspt.TEXT_DOCUMENT_DEFINITION)
153
+ async def definition(
154
+ ls: JacLangServer, params: lspt.TextDocumentPositionParams
155
+ ) -> Optional[lspt.Location]:
156
+ """Provide definition."""
157
+ stop_analysis()
158
+ analyze_and_publish(ls, params.text_document.uri, level=1)
159
+ return ls.get_definition(params.text_document.uri, params.position)
160
+
161
+
133
162
  def run_lang_server() -> None:
134
163
  """Run the language server."""
135
164
  server.start_io()