jaclang 0.8.7__py3-none-any.whl → 0.8.9__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 (99) hide show
  1. jaclang/cli/cli.py +77 -29
  2. jaclang/cli/cmdreg.py +44 -0
  3. jaclang/compiler/constant.py +6 -2
  4. jaclang/compiler/jac.lark +37 -47
  5. jaclang/compiler/larkparse/jac_parser.py +2 -2
  6. jaclang/compiler/parser.py +356 -61
  7. jaclang/compiler/passes/main/__init__.py +2 -4
  8. jaclang/compiler/passes/main/def_use_pass.py +1 -4
  9. jaclang/compiler/passes/main/predynamo_pass.py +221 -0
  10. jaclang/compiler/passes/main/pyast_gen_pass.py +221 -135
  11. jaclang/compiler/passes/main/pyast_load_pass.py +54 -20
  12. jaclang/compiler/passes/main/sym_tab_build_pass.py +1 -1
  13. jaclang/compiler/passes/main/tests/fixtures/checker/import_sym.jac +2 -0
  14. jaclang/compiler/passes/main/tests/fixtures/checker/import_sym_test.jac +6 -0
  15. jaclang/compiler/passes/main/tests/fixtures/checker/imported_sym.jac +5 -0
  16. jaclang/compiler/passes/main/tests/fixtures/checker_arg_param_match.jac +37 -0
  17. jaclang/compiler/passes/main/tests/fixtures/checker_arity.jac +18 -0
  18. jaclang/compiler/passes/main/tests/fixtures/checker_cat_is_animal.jac +18 -0
  19. jaclang/compiler/passes/main/tests/fixtures/checker_float.jac +7 -0
  20. jaclang/compiler/passes/main/tests/fixtures/checker_param_types.jac +11 -0
  21. jaclang/compiler/passes/main/tests/fixtures/checker_self_type.jac +9 -0
  22. jaclang/compiler/passes/main/tests/fixtures/checker_sym_inherit.jac +42 -0
  23. jaclang/compiler/passes/main/tests/fixtures/predynamo_fix3.jac +43 -0
  24. jaclang/compiler/passes/main/tests/fixtures/predynamo_where_assign.jac +13 -0
  25. jaclang/compiler/passes/main/tests/fixtures/predynamo_where_return.jac +11 -0
  26. jaclang/compiler/passes/main/tests/test_checker_pass.py +190 -0
  27. jaclang/compiler/passes/main/tests/test_predynamo_pass.py +56 -0
  28. jaclang/compiler/passes/main/type_checker_pass.py +29 -73
  29. jaclang/compiler/passes/tool/doc_ir_gen_pass.py +302 -58
  30. jaclang/compiler/passes/tool/jac_formatter_pass.py +119 -69
  31. jaclang/compiler/passes/tool/tests/fixtures/corelib_fmt.jac +3 -3
  32. jaclang/compiler/passes/tool/tests/fixtures/general_format_checks/triple_quoted_string.jac +4 -5
  33. jaclang/compiler/passes/tool/tests/fixtures/import_fmt.jac +7 -1
  34. jaclang/compiler/passes/tool/tests/fixtures/tagbreak.jac +276 -10
  35. jaclang/compiler/passes/transform.py +12 -8
  36. jaclang/compiler/program.py +19 -7
  37. jaclang/compiler/tests/fixtures/jac_import_py_files.py +4 -0
  38. jaclang/compiler/tests/fixtures/jac_module.jac +3 -0
  39. jaclang/compiler/tests/fixtures/multiple_syntax_errors.jac +10 -0
  40. jaclang/compiler/tests/fixtures/python_module.py +1 -0
  41. jaclang/compiler/tests/test_importer.py +39 -0
  42. jaclang/compiler/tests/test_parser.py +49 -0
  43. jaclang/compiler/type_system/type_evaluator.jac +959 -0
  44. jaclang/compiler/type_system/type_utils.py +246 -0
  45. jaclang/compiler/type_system/types.py +58 -2
  46. jaclang/compiler/unitree.py +102 -107
  47. jaclang/langserve/engine.jac +138 -159
  48. jaclang/langserve/server.jac +25 -1
  49. jaclang/langserve/tests/fixtures/circle.jac +3 -3
  50. jaclang/langserve/tests/fixtures/circle_err.jac +3 -3
  51. jaclang/langserve/tests/fixtures/circle_pure.test.jac +3 -3
  52. jaclang/langserve/tests/fixtures/completion_test_err.jac +10 -0
  53. jaclang/langserve/tests/server_test/circle_template.jac +80 -0
  54. jaclang/langserve/tests/server_test/glob_template.jac +4 -0
  55. jaclang/langserve/tests/server_test/test_lang_serve.py +154 -309
  56. jaclang/langserve/tests/server_test/utils.py +153 -116
  57. jaclang/langserve/tests/test_server.py +21 -84
  58. jaclang/langserve/utils.jac +12 -15
  59. jaclang/lib.py +17 -0
  60. jaclang/runtimelib/archetype.py +25 -25
  61. jaclang/runtimelib/constructs.py +2 -2
  62. jaclang/runtimelib/machine.py +63 -46
  63. jaclang/runtimelib/meta_importer.py +27 -1
  64. jaclang/runtimelib/tests/fixtures/custom_access_validation.jac +1 -1
  65. jaclang/runtimelib/tests/fixtures/savable_object.jac +2 -2
  66. jaclang/settings.py +19 -16
  67. jaclang/tests/fixtures/abc_check.jac +3 -3
  68. jaclang/tests/fixtures/arch_rel_import_creation.jac +12 -12
  69. jaclang/tests/fixtures/attr_pattern_case.jac +18 -0
  70. jaclang/tests/fixtures/chandra_bugs2.jac +3 -3
  71. jaclang/tests/fixtures/create_dynamic_archetype.jac +13 -13
  72. jaclang/tests/fixtures/funccall_genexpr.jac +7 -0
  73. jaclang/tests/fixtures/funccall_genexpr.py +5 -0
  74. jaclang/tests/fixtures/maxfail_run_test.jac +4 -4
  75. jaclang/tests/fixtures/params/param_syntax_err.jac +9 -0
  76. jaclang/tests/fixtures/params/test_complex_params.jac +42 -0
  77. jaclang/tests/fixtures/params/test_failing_kwonly.jac +207 -0
  78. jaclang/tests/fixtures/params/test_failing_posonly.jac +116 -0
  79. jaclang/tests/fixtures/params/test_failing_varargs.jac +300 -0
  80. jaclang/tests/fixtures/params/test_kwonly_params.jac +29 -0
  81. jaclang/tests/fixtures/py2jac_params.py +8 -0
  82. jaclang/tests/fixtures/run_test.jac +4 -4
  83. jaclang/tests/test_cli.py +159 -7
  84. jaclang/tests/test_language.py +213 -38
  85. jaclang/tests/test_reference.py +3 -1
  86. jaclang/utils/helpers.py +67 -6
  87. jaclang/utils/module_resolver.py +10 -0
  88. jaclang/utils/test.py +8 -0
  89. jaclang/utils/tests/test_lang_tools.py +4 -15
  90. jaclang/utils/treeprinter.py +0 -18
  91. {jaclang-0.8.7.dist-info → jaclang-0.8.9.dist-info}/METADATA +1 -2
  92. {jaclang-0.8.7.dist-info → jaclang-0.8.9.dist-info}/RECORD +95 -65
  93. {jaclang-0.8.7.dist-info → jaclang-0.8.9.dist-info}/WHEEL +1 -1
  94. jaclang/compiler/passes/main/inheritance_pass.py +0 -131
  95. jaclang/compiler/type_system/type_evaluator.py +0 -560
  96. jaclang/langserve/dev_engine.jac +0 -645
  97. jaclang/langserve/dev_server.jac +0 -201
  98. /jaclang/{langserve/tests/server_test/code_test.py → tests/fixtures/py2jac_empty.py} +0 -0
  99. {jaclang-0.8.7.dist-info → jaclang-0.8.9.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,959 @@
1
+ """
2
+ Type system evaluator for JacLang.
3
+
4
+ PyrightReference:
5
+ packages/pyright-internal/src/analyzer/typeEvaluator.ts
6
+ packages/pyright-internal/src/analyzer/typeEvaluatorTypes.ts
7
+ """
8
+
9
+
10
+ import ast as py_ast;
11
+ import os;
12
+ import from dataclasses { dataclass }
13
+ import from pathlib { Path }
14
+ import from typing { Callable, TYPE_CHECKING, cast }
15
+
16
+ import jaclang.compiler.unitree as uni;
17
+ import from jaclang.compiler { TOKEN_MAP }
18
+ import from jaclang.compiler.constant { Tokens as Tok }
19
+ import from jaclang.compiler.passes.main.pyast_load_pass { PyastBuildPass }
20
+ import from jaclang.compiler.passes.main.sym_tab_build_pass { SymTabBuildPass }
21
+ import from jaclang.compiler.type_system { types }
22
+ import from jaclang.runtimelib.utils { read_file_with_encoding }
23
+
24
+ with entry {
25
+ if TYPE_CHECKING {
26
+ import from jaclang.compiler.program { JacProgram }
27
+ }
28
+ }
29
+
30
+ # FIXME: Conver the bellow to dot import (from . import ...)
31
+ # Currently its not possible with jac+python hybrid repos.
32
+ import from jaclang.compiler.type_system { operations }
33
+ import from jaclang.compiler.type_system { type_utils }
34
+ import from jaclang.compiler.type_system.types { TypeBase }
35
+
36
+
37
+ # The callback type definition for the diagnostic messages.
38
+ glob DiagnosticCallback = Callable[[uni.UniNode, str, bool], None];
39
+
40
+
41
+ """Types whose definitions are prefetched and cached by the type evaluator."""
42
+ obj PrefetchedTypes {
43
+ has none_type_class: TypeBase | None = None;
44
+ has object_class: TypeBase | None = None;
45
+ has type_class: TypeBase | None = None;
46
+ has union_type_class: TypeBase | None = None;
47
+ has awaitable_class: TypeBase | None = None;
48
+ has function_class: TypeBase | None = None;
49
+ has method_class: TypeBase | None = None;
50
+ has tuple_class: TypeBase | None = None;
51
+ has bool_class: TypeBase | None = None;
52
+ has int_class: TypeBase | None = None;
53
+ has float_class: TypeBase | None = None;
54
+ has str_class: TypeBase | None = None;
55
+ has dict_class: TypeBase | None = None;
56
+ has module_type_class: TypeBase | None = None;
57
+ has typed_dict_class: TypeBase | None = None;
58
+ has typed_dict_private_class: TypeBase | None = None;
59
+ has supports_keys_and_get_item_class: TypeBase | None = None;
60
+ has mapping_class: TypeBase | None = None;
61
+ has template_class: TypeBase | None = None;
62
+ }
63
+
64
+ """Represents a single entry in the symbol resolution stack."""
65
+ obj SymbolResolutionStackEntry {
66
+
67
+ has symbol: uni.Symbol;
68
+
69
+ # Initially true, it's set to false if a recursion
70
+ # is detected.
71
+ has is_result_valid: bool = True;
72
+
73
+ # Some limited forms of recursion are allowed. In these
74
+ # cases, a partially-constructed type can be registered.
75
+ has partial_type: TypeBase | None = None;
76
+ }
77
+
78
+ """Result of matching arguments to parameters."""
79
+ obj MatchArgsToParamsResult {
80
+
81
+ # FIXME: This class implementation is modified from pyright to make it
82
+ # simple and work for now, however this needs to be revisited and
83
+ # implemented properly.
84
+ has arg_params: dict[uni.Expr | uni.KWPair, types.Parameter | None];
85
+
86
+ has overload: types.FunctionType | None = None;
87
+ has argument_errors: bool = False;
88
+ }
89
+
90
+ """Type evaluator for JacLang."""
91
+ class TypeEvaluator {
92
+
93
+ # NOTE: This is done in the binder pass of pyright, however I'm doing this
94
+ # here, cause this will be the entry point of the type checker and we're not
95
+ # relying on the binder pass at the moment and we can go back to binder pass
96
+ # in the future if we needed it.
97
+ has _BUILTINS_STUB_FILE_PATH: str = os.path.join(
98
+ os.path.dirname(__file__),
99
+ "../../vendor/typeshed/stdlib/builtins.pyi",
100
+ );
101
+
102
+ """Initialize the type evaluator with prefetched types.
103
+
104
+ Implementation Note:
105
+ --------------------
106
+ Pyright is prefetching the builtins when an evaluation is requested
107
+ on a node and from that node it does lookup for the builtins scope
108
+ and does the prefetch once, however if we forgot to call prefetch
109
+ in some place then it will not be available in the evaluator, So we
110
+ are prefetching the builtins at the constructor level once.
111
+ """
112
+ def __init__(self: TypeEvaluator, program: "JacProgram") -> None {
113
+ self.program = program;
114
+ self.symbol_resolution_stack: list[SymbolResolutionStackEntry] = [];
115
+ self.builtins_module = self._load_builtins_stub_module();
116
+ self.prefetch = self._prefetch_types();
117
+ self.diagnostic_callback: DiagnosticCallback | None = None;
118
+ }
119
+
120
+ """Load and return the builtins stub module."""
121
+ def _load_builtins_stub_module(self: TypeEvaluator) -> uni.Module {
122
+ if not os.path.exists(TypeEvaluator._BUILTINS_STUB_FILE_PATH) {
123
+ raise FileNotFoundError(
124
+ f"Builtins stub file not found at {TypeEvaluator._BUILTINS_STUB_FILE_PATH}"
125
+ );
126
+ }
127
+ file_content = read_file_with_encoding(TypeEvaluator._BUILTINS_STUB_FILE_PATH);
128
+ uni_source = uni.Source(file_content, TypeEvaluator._BUILTINS_STUB_FILE_PATH);
129
+ mod = PyastBuildPass(
130
+ ir_in=uni.PythonModuleAst(
131
+ py_ast.parse(file_content),
132
+ orig_src=uni_source,
133
+ ),
134
+ prog=self.program,
135
+ ).ir_out;
136
+ SymTabBuildPass(ir_in=mod, prog=self.program);
137
+ return mod;
138
+ }
139
+
140
+ """Return the built-in type with the given name."""
141
+ def _get_builtin_type(self: TypeEvaluator, name: str) -> TypeBase {
142
+ if (symbol := self.builtins_module.lookup(name)) is not None {
143
+ return self.get_type_of_symbol(symbol);
144
+ }
145
+ return types.UnknownType();
146
+ }
147
+
148
+ """Return the prefetched types for the type evaluator."""
149
+ def _prefetch_types(self: TypeEvaluator) -> "PrefetchedTypes" {
150
+ return PrefetchedTypes(
151
+ # TODO: Pyright first try load NoneType from typeshed and if it cannot
152
+ # then it set to unknown type.
153
+ none_type_class=types.UnknownType(),
154
+ object_class=self._get_builtin_type("object"),
155
+ type_class=self._get_builtin_type("type"),
156
+ # union_type_class=
157
+ # awaitable_class=
158
+ # function_class=
159
+ # method_class=
160
+ tuple_class=self._get_builtin_type("tuple"),
161
+ bool_class=self._get_builtin_type("bool"),
162
+ int_class=self._get_builtin_type("int"),
163
+ float_class=self._get_builtin_type("float"),
164
+ str_class=self._get_builtin_type("str"),
165
+ dict_class=self._get_builtin_type("dict"),
166
+ # module_type_class=
167
+ # typed_dict_class=
168
+ # typed_dict_private_class=
169
+ # supports_keys_and_get_item_class=
170
+ # mapping_class=
171
+ # template_class=
172
+ );
173
+ }
174
+
175
+ """Add a diagnostic message to the program."""
176
+ def add_diagnostic(
177
+ self: TypeEvaluator, node: uni.UniNode, message: str, warning: bool = False
178
+ ) -> None {
179
+ if self.diagnostic_callback {
180
+ self.diagnostic_callback(node, message, warning);
181
+ }
182
+ }
183
+
184
+ # -------------------------------------------------------------------------
185
+ # Symbol resolution stack
186
+ # -------------------------------------------------------------------------
187
+
188
+ """Get the index of a symbol in the resolution stack."""
189
+ def get_index_of_symbol_resolution(self: TypeEvaluator, symbol: uni.Symbol) -> int | None {
190
+ for (i, entry_) in enumerate(self.symbol_resolution_stack) {
191
+ if entry_.symbol == symbol {
192
+ return i;
193
+ }
194
+ }
195
+ return None;
196
+ }
197
+
198
+ """
199
+ Push a symbol onto the resolution stack.
200
+
201
+ Return False if recursion detected and in that case it won't push the symbol.
202
+ """
203
+ def push_symbol_resolution(self: TypeEvaluator, symbol: uni.Symbol) -> bool {
204
+ idx = self.get_index_of_symbol_resolution(symbol);
205
+ if idx is not None {
206
+ # Mark all of the entries between these two as invalid.
207
+ for i in range(idx, len(self.symbol_resolution_stack)) {
208
+ entry_ = self.symbol_resolution_stack[i];
209
+ entry_.is_result_valid = False;
210
+ }
211
+ return False;
212
+
213
+ }
214
+ self.symbol_resolution_stack.append(SymbolResolutionStackEntry(symbol=symbol));
215
+ return True;
216
+ }
217
+
218
+ """Pop a symbol from the resolution stack."""
219
+ def pop_symbol_resolution(self: TypeEvaluator, symbol: uni.Symbol) -> bool {
220
+ popped_entry = self.symbol_resolution_stack.pop();
221
+ assert popped_entry.symbol == symbol;
222
+ return popped_entry.is_result_valid;
223
+ }
224
+
225
+ # Pyright equivalent function name = getEffectiveTypeOfSymbol.
226
+ """Return the effective type of the symbol."""
227
+ def get_type_of_symbol(self: TypeEvaluator, symbol: uni.Symbol) -> TypeBase {
228
+ if self.push_symbol_resolution(symbol) {
229
+ try {
230
+ return self._get_type_of_symbol(symbol);
231
+ } finally {
232
+ self.pop_symbol_resolution(symbol);
233
+ }
234
+ }
235
+
236
+ # If we reached here that means we have a cyclic symbolic reference.
237
+ return types.UnknownType();
238
+ }
239
+
240
+ # NOTE: This function doesn't exists in pyright, however it exists as a helper function
241
+ # for the following functions.
242
+ """Import a module from the given path."""
243
+ def _import_module_from_path(self: TypeEvaluator, path: str) -> uni.Module {
244
+ # Get the module, if it's not loaded yet, compile and get it.
245
+ #
246
+ # TODO:
247
+ # We're not typechecking inside the module itself however we
248
+ # need to check if the module path is site-package or not and
249
+ # do typecheck inside as well.
250
+ mod: uni.Module;
251
+ if path in self.program.mod.hub {
252
+ mod = self.program.mod.hub[path];
253
+ } else {
254
+ mod = self.program.compile(path, no_cgen=True, type_check=False);
255
+ # FIXME: Inherit from builtin symbol table logic is currently implemented
256
+ # here and checker pass, however it should be in one location, since we're
257
+ # doing type_check=False, it doesn't set parent_scope to builtins, this
258
+ # needs to be done properly. The way jaclang handles symbol table is different
259
+ # than pyright, so we cannot strictly follow them, however as long as a new
260
+ # module has a parent scope as builtin scope, we're aligned with pyright.
261
+ if mod.parent_scope is None and mod is not self.builtins_module {
262
+ mod.parent_scope = self.builtins_module;
263
+ }
264
+ }
265
+ return mod;
266
+ }
267
+
268
+ """Return the effective type of the module."""
269
+ def get_type_of_module(self: TypeEvaluator, node_: uni.ModulePath) -> types.TypeBase {
270
+ if node_.name_spec.type is not None {
271
+ return cast(types.ModuleType, node_.name_spec.type);
272
+ }
273
+ if not Path(node_.resolve_relative_path()).exists() {
274
+ node_.name_spec.type = types.UnknownType();
275
+ return node_.name_spec.type;
276
+ }
277
+
278
+ mod: uni.Module = self._import_module_from_path(node_.resolve_relative_path());
279
+ mod_type = types.ModuleType(
280
+ mod_name=node_.name_spec.sym_name,
281
+ file_uri=Path(node_.resolve_relative_path()).resolve(),
282
+ symbol_table=mod,
283
+ );
284
+
285
+ node_.name_spec.type = mod_type;
286
+ return mod_type;
287
+ }
288
+
289
+ """Return the effective type of the module item."""
290
+ def get_type_of_module_item(self: TypeEvaluator, node_: uni.ModuleItem) -> types.TypeBase {
291
+ # Module item can be both a module or a member of a module.
292
+ # import from .. { mod } # <-- Here mod is not a member but a module itself.
293
+ # import from mod { item } # <-- Here item is not a module but a member of mod.
294
+ if node_.name_spec.type is not None {
295
+ return node_.name_spec.type;
296
+ }
297
+
298
+ import_node = node_.parent_of_type(uni.Import);
299
+ if import_node.from_loc {
300
+
301
+ from_path = Path(import_node.from_loc.resolve_relative_path());
302
+ is_dir = from_path.is_dir() or (from_path.stem == "__init__");
303
+
304
+ # import from .. { mod }
305
+ if is_dir {
306
+ mod_dir = from_path.parent if not from_path.is_dir() else from_path;
307
+ # FIXME: Implement module resolution properly.
308
+ for ext in (".jac", ".py", ".pyi") {
309
+ if (path := (mod_dir / (node_.name.value + ext)).resolve()).exists() {
310
+ mod = self._import_module_from_path(str(path));
311
+ mod_type = types.ModuleType(
312
+ mod_name=node_.name_spec.sym_name,
313
+ file_uri=path,
314
+ symbol_table=mod,
315
+ );
316
+ # Cache the type.
317
+ node_.name_spec.type = mod_type;
318
+
319
+ # FIXME: goto definition works on imported symbol by checking if it's a MODULE
320
+ # type and in that case it'll call resolve_relative_path on the parent node of
321
+ # the symbol's definition node (a module path), So the goto definition to work
322
+ # properly the category should be module on a module path, If we set like this
323
+ # below should work but because of the above assumption (should be a mod path)
324
+ # it won't, This needs to be discussed.
325
+ #
326
+ # node.name_spec._sym_category = uni.SymbolType.MODULE
327
+ return node_.name_spec.type;
328
+ }
329
+ }
330
+ }
331
+
332
+ # import from mod { item }
333
+ else {
334
+ mod_type = self.get_type_of_module(import_node.from_loc);
335
+ if not isinstance(mod_type, types.ModuleType) {
336
+ node_.name_spec.type = types.UnknownType();
337
+ # TODO: Add diagnostic that from_loc is not accessible.
338
+ # Eg: 'Import "scipy" could not be resolved'
339
+ return node_.name_spec.type;
340
+ }
341
+ if sym := mod_type.symbol_table.lookup(node_.name.value, deep=True) {
342
+ node_.name.sym = sym;
343
+ if node_.alias {
344
+ node_.alias.sym = sym;
345
+ }
346
+ node_.name_spec.type = self.get_type_of_symbol(sym);
347
+ return node_.name_spec.type;
348
+ }
349
+ }
350
+ }
351
+
352
+ return types.UnknownType();
353
+ }
354
+
355
+ """Return the effective type of the class."""
356
+ def get_type_of_class(self: TypeEvaluator, node_: uni.Archetype) -> types.ClassType {
357
+ # Is this type already cached?
358
+ if node_.name_spec.type is not None {
359
+ return cast(types.ClassType, node_.name_spec.type);
360
+ }
361
+
362
+ base_classes: list[TypeBase] = [];
363
+ for base_class in node_.base_classes or [] {
364
+ base_class_type = self.get_type_of_expression(base_class);
365
+ base_classes.append(base_class_type);
366
+ }
367
+ is_builtin_class = node_.find_parent_of_type(uni.Module) == self.builtins_module;
368
+
369
+ cls_type = types.ClassType(
370
+ types.ClassType.ClassDetailsShared(
371
+ class_name=node_.name_spec.sym_name,
372
+ symbol_table=node_,
373
+ base_classes=base_classes,
374
+ is_builtin_class=is_builtin_class,
375
+ ),
376
+ flags=types.TypeFlags.Instantiable,
377
+ );
378
+
379
+ # Compute the MRO for the class.
380
+ type_utils.compute_mro_linearization(cls_type);
381
+
382
+ # Cache the type, pyright is doing invalidateTypeCacheIfCanceled()
383
+ # we're not doing that any time sooner.
384
+ node_.name_spec.type = cls_type;
385
+ return cls_type;
386
+ }
387
+
388
+ """Return the effective type of an ability."""
389
+ def get_type_of_ability(self: TypeEvaluator, node_: uni.Ability) -> TypeBase {
390
+ if node_.name_spec.type is not None {
391
+ return node_.name_spec.type;
392
+ }
393
+
394
+ if not isinstance(node_.signature, uni.FuncSignature) {
395
+ node_.name_spec.type = types.UnknownType();
396
+ return node_.name_spec.type;
397
+ }
398
+
399
+ return_type: TypeBase;
400
+ if isinstance(node_.signature.return_type, uni.Expr) {
401
+ return_type = self._convert_to_instance(
402
+ self.get_type_of_expression(node_.signature.return_type)
403
+ );
404
+ } else {
405
+ return_type = types.UnknownType();
406
+ }
407
+
408
+ # Define helper function for parameter conversion.
409
+ def _get_param_category(param: uni.ParamVar) -> types.ParameterCategory {
410
+ if param.is_vararg {
411
+ return types.ParameterCategory.ArgsList;
412
+ }
413
+ if param.is_kwargs {
414
+ return types.ParameterCategory.KwargsDict;
415
+ }
416
+ return types.ParameterCategory.Positional;
417
+ }
418
+
419
+ # Define helper function for parameter kind conversion.
420
+ def _convert_param_kind(kind: uni.ParamKind) -> types.ParamKind {
421
+ ::py::
422
+ match kind:
423
+ case uni.ParamKind.POSONLY:
424
+ return types.ParamKind.POSONLY
425
+ case uni.ParamKind.NORMAL:
426
+ return types.ParamKind.NORMAL
427
+ case uni.ParamKind.VARARG:
428
+ return types.ParamKind.VARARG
429
+ case uni.ParamKind.KWONLY:
430
+ return types.ParamKind.KWONLY
431
+ case uni.ParamKind.KWARG:
432
+ return types.ParamKind.KWARG
433
+ return types.ParamKind.NORMAL
434
+ ::py::
435
+ }
436
+
437
+ parameters: list[types.Parameter] = [];
438
+ for (idx, param) in enumerate(node_.signature.get_parameters()) {
439
+ # TODO: Set parameter category for *args, and **kwargs
440
+ param_type: TypeBase | None = None;
441
+
442
+ if param.type_tag {
443
+ param_type_cls = self.get_type_of_expression(param.type_tag.tag);
444
+ param_type = self._convert_to_instance(param_type_cls);
445
+ }
446
+
447
+ parameters.append(
448
+ types.Parameter(
449
+ name=param.name.value,
450
+ category=_get_param_category(param),
451
+ param_type=param_type,
452
+ default_value=param.value,
453
+ is_self=(idx == 0 and self._is_expr_self(param.name)),
454
+ param_kind=_convert_param_kind(param.param_kind),
455
+ )
456
+ );
457
+ }
458
+
459
+ func_type = types.FunctionType(
460
+ func_name=node_.name_spec.sym_name,
461
+ return_type=return_type,
462
+ parameters=parameters,
463
+ );
464
+
465
+ node_.name_spec.type = func_type;
466
+ return func_type;
467
+ }
468
+
469
+ """Return the effective type of the string."""
470
+ def get_type_of_string(self: TypeEvaluator, node: uni.String | uni.MultiString) -> TypeBase {
471
+ # FIXME: Strings are a type of LiteralString type:
472
+ # "foo" is not `str` but Literal["foo"], however for now we'll
473
+ # not considering that and make it work and will implement that
474
+ # later.
475
+ #
476
+ # see: getTypeOfString() in pyright (it requires parsing the sub
477
+ # file of the typing module).
478
+ assert self.prefetch.str_class is not None;
479
+ return self.prefetch.str_class;
480
+ }
481
+
482
+ """Return the effective type of the int."""
483
+ def get_type_of_int(self: TypeEvaluator, node: uni.Int) -> TypeBase {
484
+ assert self.prefetch.int_class is not None;
485
+ return self.prefetch.int_class;
486
+ }
487
+
488
+ """Return the effective type of the float."""
489
+ def get_type_of_float(self: TypeEvaluator, node: uni.Float) -> TypeBase {
490
+ assert self.prefetch.float_class is not None;
491
+ return self.prefetch.float_class;
492
+ }
493
+
494
+ # Pyright equivalent function name = getTypeOfExpression();
495
+ """Return the effective type of the expression."""
496
+ def get_type_of_expression(self: TypeEvaluator, node_: uni.Expr) -> TypeBase {
497
+ # If it's alreay "cached" return it.
498
+ if node_.type is not None {
499
+ return node_.type;
500
+ }
501
+
502
+ result = self._get_type_of_expression_core(node_);
503
+ # If the context has an expected type, pyright does a compatibility and set
504
+ # a diagnostics here, I don't understand why that might be necessary here.
505
+
506
+ node_.type = result; # Cache the result
507
+ return result;
508
+ }
509
+
510
+ # Comments from pyright:
511
+ # // Determines if the source type can be assigned to the dest type.
512
+ # // If constraint are provided, type variables within the destType are
513
+ # // matched against existing type variables in the map. If a type variable
514
+ # // in the dest type is not in the type map already, it is assigned a type
515
+ # // and added to the map.
516
+ """Assign the source type to the destination type."""
517
+ def assign_type(self: TypeEvaluator, src_type: TypeBase, dest_type: TypeBase) -> bool {
518
+ if types.TypeCategory.Unknown in (src_type.category, dest_type.category) {
519
+ # NOTE: For now if we don't have the type info, we assume it's compatible.
520
+ # For strict mode we should disallow usage of unknown unless explicitly ignored.
521
+ return True;
522
+ }
523
+
524
+ if src_type == dest_type {
525
+ return True;
526
+ }
527
+
528
+ if dest_type.is_class_instance() and src_type.is_class_instance() {
529
+ assert isinstance(dest_type, types.ClassType);
530
+ assert isinstance(src_type, types.ClassType);
531
+ return self._assign_class(src_type, dest_type);
532
+ }
533
+
534
+ return False;
535
+ }
536
+
537
+ # TODO: This should take an argument list as parameter.
538
+ """Return the effective return type of a magic method call."""
539
+ def get_type_of_magic_method_call(self: TypeEvaluator, obj_type: TypeBase, method_name: str) -> TypeBase | None {
540
+ if obj_type.category == types.TypeCategory.Class {
541
+ # TODO: getTypeOfBoundMember() <-- Implement this if needed, for the simple case
542
+ # we'll directly call member lookup.
543
+ #
544
+ # WE'RE DAVIATING FROM PYRIGHT FOR THIS METHOD HEAVILY HOWEVER THIS CAN BE RE-WRITTEN IF NEEDED.
545
+ #
546
+ assert isinstance(obj_type, types.ClassType); # <-- To make typecheck happy.
547
+ if member := self._lookup_class_member(obj_type, method_name) {
548
+ member_ty = self.get_type_of_symbol(member.symbol);
549
+ if isinstance(member_ty, types.FunctionType) {
550
+ return member_ty.return_type;
551
+ }
552
+ # If we reached here, magic method is not a function.
553
+ # 1. recursively check __call__() on the type, TODO
554
+ # 2. if any or unknown, return getUnknownTypeForCallable() TODO
555
+ # 3. return undefined.
556
+ return None;
557
+ }
558
+ }
559
+ return None;
560
+ }
561
+
562
+ """Assign the source class type to the destination class type."""
563
+ def _assign_class(self: TypeEvaluator, src_type: types.ClassType, dest_type: types.ClassType) -> bool {
564
+ if src_type.shared == dest_type.shared {
565
+ return True;
566
+ }
567
+
568
+ # Check if src class is a subclass of dest class.
569
+ for base_cls in src_type.shared.mro {
570
+ if base_cls.shared == dest_type.shared {
571
+ return True;
572
+ }
573
+ }
574
+
575
+ # Everything is assignable to an object.
576
+ if dest_type.is_builtin("object") {
577
+ # TODO: Invariance not handled yet
578
+ # invariant contexts to avoid list[int] <: list[object] errors.
579
+ return True;
580
+ }
581
+
582
+ # Integers can be used where floats are expected.
583
+ if src_type.is_builtin("int") and dest_type.is_builtin("float") {
584
+ return True;
585
+ }
586
+
587
+ # TODO: Search base classes and everything else pyright is doing.
588
+ return False;
589
+ }
590
+
591
+ # This function is a combination of the bellow pyright functions.
592
+ # - getDeclaredTypeOfSymbol
593
+ # - getTypeForDeclaration
594
+ #
595
+ # Implementation Note:
596
+ # Pyright is actually have some duplicate logic for handling declared
597
+ # type and inferred type, we're going to unify them (if it's required
598
+ # in the future, we can refactor this).
599
+ """Return the declared type of the symbol."""
600
+ def _get_type_of_symbol(self: TypeEvaluator, symbol: uni.Symbol) -> TypeBase {
601
+ node_ = symbol.decl.name_of;
602
+ match node_ {
603
+ case uni.ModulePath():
604
+ return self.get_type_of_module(node_);
605
+
606
+ case uni.ModuleItem():
607
+ return self.get_type_of_module_item(node_);
608
+
609
+ case uni.Archetype():
610
+ return self.get_type_of_class(node_);
611
+
612
+ case uni.Ability():
613
+ return self.get_type_of_ability(node_);
614
+
615
+ case uni.ParamVar():
616
+ if node_.type_tag {
617
+ annotation_type = self.get_type_of_expression(node_.type_tag.tag);
618
+ return self._convert_to_instance(annotation_type);
619
+ }
620
+
621
+ # This actually defined in the function getTypeForDeclaration();
622
+ # Pyright has DeclarationType.Variable.
623
+ case uni.Name():
624
+ if isinstance(node_.parent, uni.Assignment) {
625
+ if node_.parent.type_tag is not None {
626
+ annotation_type = self.get_type_of_expression(
627
+ node_.parent.type_tag.tag
628
+ );
629
+ return self._convert_to_instance(annotation_type);
630
+ }
631
+
632
+ else { # Assignment without a type annotation.
633
+ if node_.parent.value is not None {
634
+ return self.get_type_of_expression(node_.parent.value);
635
+ }
636
+ }
637
+ }
638
+
639
+ case uni.HasVar():
640
+ if node_.type_tag is not None {
641
+ annotation_type = self.get_type_of_expression(node_.type_tag.tag);
642
+ return self._convert_to_instance(annotation_type);
643
+ } else {
644
+ if node_.value is not None {
645
+ return self.get_type_of_expression(node_.value);
646
+ }
647
+ }
648
+ }
649
+
650
+ # TODO: Implement for functions, parameters, explicit type
651
+ # annotations in assignment etc.
652
+ return types.UnknownType();
653
+ }
654
+
655
+ # Pyright equivalent function name = getTypeOfExpressionCore();
656
+ """Core function to get the type of the expression."""
657
+ def _get_type_of_expression_core(self: TypeEvaluator, expr: uni.Expr) -> TypeBase {
658
+ match expr {
659
+
660
+ case uni.String() | uni.MultiString():
661
+ return self._convert_to_instance(self.get_type_of_string(expr));
662
+
663
+ case uni.Int():
664
+ return self._convert_to_instance(self.get_type_of_int(expr));
665
+
666
+ case uni.Float():
667
+ return self._convert_to_instance(self.get_type_of_float(expr));
668
+
669
+ case uni.AtomTrailer():
670
+ # NOTE: Pyright is using CFG to figure out the member type by narrowing the base
671
+ # type and filtering the members. We're not doing that anytime sooner.
672
+ base_type = self.get_type_of_expression(expr.target);
673
+
674
+ if expr.is_attr { # <expr>.member
675
+ assert isinstance(expr.right, uni.Name);
676
+
677
+ if isinstance(base_type, types.ModuleType) {
678
+ # getTypeOfMemberAccessWithBaseType()
679
+ if sym := base_type.symbol_table.lookup(
680
+ expr.right.value, deep=True
681
+ ) {
682
+ expr.right.sym = sym;
683
+ return self.get_type_of_symbol(sym);
684
+ }
685
+ return types.UnknownType();
686
+ }
687
+
688
+ elif base_type.is_instantiable_class() {
689
+ assert isinstance(base_type, types.ClassType);
690
+ if member := self._lookup_class_member(
691
+ base_type, expr.right.value
692
+ ) {
693
+ expr.right.sym = member.symbol;
694
+ return self.get_type_of_symbol(member.symbol);
695
+ }
696
+ return types.UnknownType();
697
+ }
698
+
699
+ elif base_type.is_class_instance() {
700
+ assert isinstance(base_type, types.ClassType);
701
+ if member := self._lookup_object_member(
702
+ base_type, expr.right.value
703
+ ) {
704
+ expr.right.sym = member.symbol;
705
+ return self.get_type_of_symbol(member.symbol);
706
+ }
707
+ return types.UnknownType();
708
+ }
709
+ }
710
+
711
+ elif expr.is_null_ok { # <expr>?.member
712
+ # TODO:
713
+ }
714
+
715
+ else { # <expr>[<expr>]
716
+ # TODO:
717
+ }
718
+
719
+ case uni.AtomUnit():
720
+ return self.get_type_of_expression(expr.value);
721
+
722
+ case uni.FuncCall():
723
+ return self.validate_call_args(expr);
724
+
725
+ case uni.BinaryExpr():
726
+ return operations.get_type_of_binary_operation(self, expr);
727
+
728
+ case uni.Name():
729
+ # NOTE: For self's type pyright is getting the first parameter of a method and
730
+ # the name can be anything not just self, however we don't have the first parameter
731
+ # and self is a keyword, we need to do it in this way.
732
+ if self._is_expr_self(expr) {
733
+ return self._get_type_of_self(expr);
734
+ }
735
+
736
+ if symbol := expr.sym_tab.lookup(expr.value, deep=True) {
737
+ expr.sym = symbol;
738
+ return self.get_type_of_symbol(symbol);
739
+ }
740
+ }
741
+
742
+ # TODO: More expressions.
743
+ return types.UnknownType();
744
+ }
745
+
746
+ # -----------------------------------------------------------------------------
747
+ # Helper functions
748
+ # -----------------------------------------------------------------------------
749
+
750
+ """Check if the expression is Name that is 'self' and in the method context."""
751
+ def _is_expr_self(self: TypeEvaluator, expr: uni.Expr) -> bool {
752
+ if (
753
+ isinstance(expr, uni.Name)
754
+ and (expr.value == TOKEN_MAP[Tok.KW_SELF])
755
+ and (fn := self._get_enclosing_method(expr))
756
+ and (not fn.is_static)
757
+ ) {
758
+ return True;
759
+ }
760
+ return False;
761
+ }
762
+
763
+ """Get the enclosing function (ability) of the given node."""
764
+ def _get_enclosing_function(self: TypeEvaluator, node: uni.UniNode) -> uni.Ability | None {
765
+ if (impl := node.find_parent_of_type(uni.ImplDef)) and (
766
+ isinstance(impl.decl_link, uni.Ability)
767
+ ) {
768
+ return impl.decl_link;
769
+ }
770
+ return node.find_parent_of_type(uni.Ability);
771
+ }
772
+
773
+ """Get the enclosing method (ability) of the given node."""
774
+ def _get_enclosing_method(self: TypeEvaluator, node: uni.UniNode) -> uni.Ability | None {
775
+ enclosing_fn = self._get_enclosing_function(node);
776
+ while enclosing_fn and (not enclosing_fn.is_method) {
777
+ enclosing_fn = self._get_enclosing_function(enclosing_fn);
778
+ }
779
+ if enclosing_fn and enclosing_fn.is_method {
780
+ return enclosing_fn;
781
+ }
782
+ return None;
783
+ }
784
+
785
+ """Return the effective type of self."""
786
+ def _get_type_of_self(self: TypeEvaluator, node: uni.Name) -> TypeBase {
787
+ if method := self._get_enclosing_method(node) {
788
+ cls = method.method_owner;
789
+ if isinstance(cls, uni.Archetype) {
790
+ return self.get_type_of_class(cls).clone_as_instance();
791
+ }
792
+ if isinstance(cls, uni.Enum) {
793
+ # TODO: Implement type from enum.
794
+ }
795
+ }
796
+ return types.UnknownType();
797
+ }
798
+
799
+ """Convert a class type to an instance type."""
800
+ def _convert_to_instance(self: TypeEvaluator, jtype: TypeBase) -> TypeBase {
801
+ # TODO: Grep pyright "Handle type[x] as a special case." They handle `type[x]` as a special case:
802
+ #
803
+ # foo: int = 42; # <-- Here `int` is instantiable class and, become instance after this method.
804
+ # foo: type[int] = int # <-- Here `type[int]`, this should be `int` that's instantiable.
805
+ #
806
+ if jtype.is_instantiable_class() {
807
+ assert isinstance(jtype, types.ClassType);
808
+ return jtype.clone_as_instance();
809
+ }
810
+ return jtype;
811
+ }
812
+
813
+ """Lookup the class member type."""
814
+ def _lookup_class_member(self: TypeEvaluator, base_type: types.ClassType, member: str) -> type_utils.ClassMember | None {
815
+ assert self.prefetch.int_class is not None;
816
+ # FIXME: Pyright's way: Implement class member iterator (based on mro and the multiple inheritance)
817
+ # return the first found member from the iterator.
818
+
819
+ # NOTE: This is a simple implementation to make it work and more robust implementation will
820
+ # be done in a future PR.
821
+ for cls in base_type.shared.mro {
822
+ if sym := cls.lookup_member_symbol(member) {
823
+ return type_utils.ClassMember(sym, cls);
824
+ }
825
+ }
826
+ return None;
827
+ }
828
+
829
+ """Lookup the object member type."""
830
+ def _lookup_object_member(self: TypeEvaluator, base_type: types.ClassType, member: str) -> type_utils.ClassMember | None {
831
+ assert self.prefetch.int_class is not None;
832
+ if base_type.is_class_instance() {
833
+ assert isinstance(base_type, types.ClassType);
834
+ # TODO: We need to implement Member lookup flags and set SkipInstanceMember to 0.
835
+ return self._lookup_class_member(base_type, member);
836
+ }
837
+ return None;
838
+ }
839
+
840
+ """
841
+ Match arguments passed to a function to the corresponding parameters in that function.
842
+
843
+ This matching is done based on positions and keywords. Type evaluation and
844
+ validation is left to the caller.
845
+ This logic is based on PEP 3102: https://www.python.org/dev/peps/pep-3102/
846
+ """
847
+ def match_args_to_params(self: TypeEvaluator, expr: uni.FuncCall, func_type: types.FunctionType) -> MatchArgsToParamsResult {
848
+ arg_params: dict[uni.Expr | uni.KWPair, types.Parameter | None] = {};
849
+ argument_errors = False;
850
+
851
+ params_to_match = func_type.parameters.copy();
852
+
853
+ # Skip `self` for method calls.
854
+ if len(func_type.parameters) >= 1 and func_type.parameters[0].is_self {
855
+ params_to_match.pop(0);
856
+ }
857
+
858
+ # Create a tracker for parameter assignment.
859
+ param_tracker = type_utils.ParamAssignmentTracker(params_to_match);
860
+
861
+ # We iterate over the arguments and match with the parameter, the param_tracker will
862
+ # keep track of the matched parameters and unmatched required parameters.
863
+ #
864
+ # Tracker: p1, p2, /, p3, p4, *args, | p6, **kwargs
865
+ # ^ ^ ^ ^ ^^ ^ | ^ ^^
866
+ # | | | | | \__ \__ | | | \________
867
+ # Args: a1, a2, a3, p4=a4, a5, a6, *a7, | p6=a8, p_kw1=a9, p_kw2=a10
868
+ # '--------------------------------' | '------------------------'
869
+ # We match positional with | We match named arguments with
870
+ # tracked parameter index. | param name lookup.
871
+ #
872
+ for arg in expr.params {
873
+ try {
874
+ if isinstance(arg, uni.KWPair) {
875
+ # Match parameter based on name lookup.
876
+ matching_param = param_tracker.match_named_argument(arg);
877
+ arg_params[arg] = matching_param;
878
+ } else { # Match parameter based on the position of the argument.
879
+ matching_param = param_tracker.match_positional_argument(arg);
880
+ arg_params[arg] = matching_param;
881
+ }
882
+ } except Exception as e {
883
+ self.add_diagnostic(arg, str(e));
884
+ argument_errors = True;
885
+ }
886
+ }
887
+
888
+ if unmatched_params := param_tracker.get_unmatched_required_params() {
889
+ names = ", ".join([ f"'{p.name}'" for p in unmatched_params ]);
890
+ argument_errors = True;
891
+ self.add_diagnostic(
892
+ expr,
893
+ f"Not all required parameters were provided in the function call: {names}",
894
+ );
895
+ }
896
+
897
+ return MatchArgsToParamsResult(
898
+ arg_params=arg_params, argument_errors=argument_errors
899
+ );
900
+ }
901
+
902
+ """
903
+ Validate that the arguments can be assigned to the call's parameter list.
904
+
905
+ Specializes the call based on arg types, and returns the specialized
906
+ type of the return value. If it detects an error along the way, it emits
907
+ a diagnostic and sets argumentErrors to true.
908
+ """
909
+ def validate_call_args(self: TypeEvaluator, expr: uni.FuncCall) -> TypeBase {
910
+ caller_type = self.get_type_of_expression(expr.target);
911
+ if isinstance(caller_type, types.FunctionType) {
912
+ arg_param_match = self.match_args_to_params(expr, caller_type);
913
+ if not arg_param_match.argument_errors {
914
+ self.validate_arg_types(arg_param_match);
915
+ }
916
+ return caller_type.return_type or types.UnknownType();
917
+ }
918
+
919
+ if (
920
+ isinstance(caller_type, types.ClassType)
921
+ and caller_type.is_instantiable_class()
922
+ ) {
923
+ # TODO: validate args for __init__()
924
+ return caller_type.clone_as_instance();
925
+ }
926
+
927
+ if caller_type.is_class_instance() {
928
+ # TODO: validate args.
929
+ magic_call_ret = self.get_type_of_magic_method_call(caller_type, "__call__");
930
+ if magic_call_ret {
931
+ return magic_call_ret;
932
+ }
933
+ }
934
+
935
+ return types.UnknownType();
936
+ }
937
+
938
+ """Validate that the argument types can be assigned to the parameter types."""
939
+ def validate_arg_types(self: TypeEvaluator, args: MatchArgsToParamsResult) -> None {
940
+ for (arg, param) in args.arg_params.items() {
941
+ if param is None or param.param_type is None {
942
+ continue;
943
+ }
944
+ if isinstance(arg, uni.KWPair) {
945
+ arg_type = self.get_type_of_expression(arg.value);
946
+ } else {
947
+ arg_type = self.get_type_of_expression(arg);
948
+ }
949
+
950
+ if not self.assign_type(arg_type, param.param_type) {
951
+ self.add_diagnostic(
952
+ arg,
953
+ f"Cannot assign {arg_type} to parameter '{param.name}' of type {param.param_type}",
954
+ );
955
+ }
956
+ }
957
+ }
958
+
959
+ }