jaclang 0.8.6__py3-none-any.whl → 0.8.7__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 (32) hide show
  1. jaclang/cli/cli.md +3 -3
  2. jaclang/cli/cli.py +25 -11
  3. jaclang/cli/cmdreg.py +1 -140
  4. jaclang/compiler/passes/main/pyast_gen_pass.py +13 -3
  5. jaclang/compiler/passes/main/pyast_load_pass.py +14 -20
  6. jaclang/compiler/passes/main/tests/fixtures/checker_binary_op.jac +21 -0
  7. jaclang/compiler/passes/main/tests/fixtures/checker_call_expr_class.jac +12 -0
  8. jaclang/compiler/passes/main/tests/fixtures/checker_cyclic_symbol.jac +4 -0
  9. jaclang/compiler/passes/main/tests/fixtures/checker_expr_call.jac +9 -0
  10. jaclang/compiler/passes/main/tests/fixtures/checker_import_missing_module.jac +13 -0
  11. jaclang/compiler/passes/main/tests/fixtures/checker_magic_call.jac +17 -0
  12. jaclang/compiler/passes/main/tests/fixtures/checker_mod_path.jac +8 -0
  13. jaclang/compiler/passes/main/tests/test_checker_pass.py +74 -0
  14. jaclang/compiler/passes/main/type_checker_pass.py +24 -5
  15. jaclang/compiler/type_system/operations.py +104 -0
  16. jaclang/compiler/type_system/type_evaluator.py +141 -2
  17. jaclang/langserve/engine.jac +135 -91
  18. jaclang/langserve/server.jac +21 -14
  19. jaclang/langserve/tests/server_test/test_lang_serve.py +2 -5
  20. jaclang/langserve/tests/test_dev_server.py +1 -1
  21. jaclang/langserve/tests/test_server.py +9 -2
  22. jaclang/langserve/utils.jac +44 -48
  23. jaclang/tests/fixtures/jac_run_py_bugs.py +18 -0
  24. jaclang/tests/fixtures/jac_run_py_import.py +13 -0
  25. jaclang/tests/fixtures/lambda_arg_annotation.jac +15 -0
  26. jaclang/tests/fixtures/lambda_self.jac +18 -0
  27. jaclang/tests/test_cli.py +66 -17
  28. jaclang/utils/module_resolver.py +1 -1
  29. {jaclang-0.8.6.dist-info → jaclang-0.8.7.dist-info}/METADATA +3 -2
  30. {jaclang-0.8.6.dist-info → jaclang-0.8.7.dist-info}/RECORD +32 -20
  31. {jaclang-0.8.6.dist-info → jaclang-0.8.7.dist-info}/WHEEL +1 -1
  32. {jaclang-0.8.6.dist-info → jaclang-0.8.7.dist-info}/entry_points.txt +0 -0
jaclang/cli/cli.md CHANGED
@@ -1,12 +1,12 @@
1
1
  # **Jac Language Command Line Interface (CLI)**
2
2
 
3
- Jac Language CLI is with a variety of commands to facilitate users. Additionally, Jac language offers users the ability to define custom CLI commands through plugins. This document aims to provide an overview of each command along with clear usage instructions.
3
+ git Jac Language CLI provides a variety of commands to facilitate users. Additionally, Jac language offers users the ability to define custom CLI commands through plugins. This document aims to provide an overview of each command along with clear usage instructions.
4
4
 
5
5
  > [!TIP]
6
- > Type "help" on Jac CLI and see!
6
+ > Use `jac --help` to see available commands and usage.
7
7
 
8
8
  ### Click one of the default commands below and see the usage.
9
- - [tool](#tool) , [run](#run) , [clean](#clean) , [format](#format) , [check](#check) , [build](#build) , [enter](#enter) , [test](#test)
9
+ - [tool](#tool) , [run](#run) , [clean](#clean) , [format](#format) , [check](#check) , [build](#build) , [enter](#enter) , [test](#test)
10
10
 
11
11
 
12
12
 
jaclang/cli/cli.py CHANGED
@@ -11,7 +11,7 @@ from pathlib import Path
11
11
  from typing import Optional
12
12
 
13
13
  import jaclang.compiler.unitree as uni
14
- from jaclang.cli.cmdreg import CommandShell, cmd_registry
14
+ from jaclang.cli.cmdreg import cmd_registry
15
15
  from jaclang.compiler.passes.main import PyastBuildPass
16
16
  from jaclang.compiler.program import JacProgram
17
17
  from jaclang.runtimelib.builtin import printgraph
@@ -132,7 +132,7 @@ def run(
132
132
  jac run myprogram.jac --session mysession
133
133
  jac run myprogram.jac --no-main
134
134
  """
135
- # if no session specified, check if it was defined when starting the command shell
135
+ # if no session specified, check if it was defined via global CLI args
136
136
  # otherwise default to jaclang.session
137
137
  base, mod, mach = proc_file_sess(filename, session)
138
138
  lng = filename.split(".")[-1]
@@ -662,6 +662,14 @@ def start_cli() -> None:
662
662
  - None
663
663
  """
664
664
  parser = cmd_registry.parser
665
+ # Default to `run` when a file is provided without a subcommand
666
+ raw_argv = sys.argv[1:]
667
+ if (
668
+ raw_argv
669
+ and not raw_argv[0].startswith("-")
670
+ and raw_argv[0].lower().endswith((".jac", ".jir", ".py"))
671
+ ):
672
+ sys.argv = [sys.argv[0], "run"] + raw_argv
665
673
  args = parser.parse_args()
666
674
  cmd_registry.args = args
667
675
 
@@ -671,16 +679,22 @@ def start_cli() -> None:
671
679
  print("Jac path:", __file__)
672
680
  return
673
681
 
682
+ if args.command is None:
683
+ parser.print_help()
684
+ return
685
+
674
686
  command = cmd_registry.get(args.command)
675
- if command:
676
- args_dict = vars(args)
677
- args_dict.pop("command")
678
- args_dict.pop("version", None)
679
- ret = command.call(**args_dict)
680
- if ret:
681
- print(ret)
682
- else:
683
- CommandShell(cmd_registry).cmdloop()
687
+ if not command:
688
+ print(f"Unknown command: {args.command}", file=sys.stderr)
689
+ parser.print_help()
690
+ return
691
+
692
+ args_dict = vars(args)
693
+ args_dict.pop("command")
694
+ args_dict.pop("version", None)
695
+ ret = command.call(**args_dict)
696
+ if ret:
697
+ print(ret)
684
698
 
685
699
 
686
700
  if __name__ == "__main__":
jaclang/cli/cmdreg.py CHANGED
@@ -3,9 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import argparse
6
- import cmd
7
6
  import inspect
8
- import pprint
9
7
  import re
10
8
  from typing import Callable, Dict, Optional
11
9
 
@@ -241,141 +239,4 @@ class CommandRegistry:
241
239
  cmd_registry = CommandRegistry()
242
240
 
243
241
 
244
- class CommandShell(cmd.Cmd):
245
- """Command shell for the command line interface."""
246
-
247
- intro = "Welcome to the Jac CLI!"
248
- prompt = "jac> "
249
- cmd_reg: CommandRegistry
250
-
251
- def __init__(self, cmd_reg: CommandRegistry) -> None:
252
- """Initialize a CommandShell instance."""
253
- self.cmd_reg = cmd_reg
254
- super().__init__()
255
-
256
- def do_exit(self, arg: list) -> bool:
257
- """Exit the command shell."""
258
- return True
259
-
260
- def default(self, line: str) -> None:
261
- """Process the command line input."""
262
- args = vars(self.cmd_reg.parser.parse_args(line.split()))
263
- command = self.cmd_reg.get(args["command"])
264
- if command:
265
- args.pop("command")
266
- ret = command.call(**args)
267
- if ret:
268
- ret_str = pprint.pformat(ret, indent=2)
269
- self.stdout.write(f"{ret_str}\n")
270
-
271
- def do_help(self, arg: str) -> None:
272
- """Jac CLI help implementation."""
273
-
274
- def get_info(name: str, doc: str, args: dict[str, inspect.Parameter]) -> None:
275
- """Format and display detailed command information."""
276
- # Format the command header
277
- self.stdout.write(f"\n{'=' * 80}\n")
278
- self.stdout.write(f"COMMAND: {name}\n")
279
- self.stdout.write(f"{'=' * 80}\n\n")
280
-
281
- # Format the command description
282
- doc_lines = doc.strip().split("\n")
283
- for line in doc_lines:
284
- self.stdout.write(f"{line}\n")
285
-
286
- # Format the command arguments
287
- if args:
288
- self.stdout.write("\nARGUMENTS:\n")
289
- for param_name, param in args.items():
290
- # Get parameter type
291
- type_name = (
292
- param.annotation.__name__
293
- if hasattr(param.annotation, "__name__")
294
- else str(param.annotation)
295
- )
296
-
297
- # Format default value if present
298
- default_str = ""
299
- if param.default is not param.empty:
300
- default_str = f" (default: {param.default})"
301
-
302
- # Format required/optional status
303
- req_str = (
304
- " [required]"
305
- if param.default is param.empty and param_name != "args"
306
- else " [optional]"
307
- )
308
-
309
- self.stdout.write(
310
- f" {param_name}: {type_name}{default_str}{req_str}\n"
311
- )
312
- else:
313
- self.stdout.write("\nNo arguments\n")
314
-
315
- # Format usage examples
316
- command_parser = self.cmd_reg.sub_parsers.choices[name]
317
- self.stdout.write(f"\nUSAGE:\n {command_parser.format_usage()[7:]}\n")
318
-
319
- # Extract and format examples from docstring if present
320
- if "Examples:" in doc:
321
- examples_section = doc.split("Examples:")[1].strip()
322
- example_lines = examples_section.split("\n")
323
- self.stdout.write("\nEXAMPLES:\n")
324
- for example in example_lines:
325
- if example.strip():
326
- self.stdout.write(f" {example.strip()}\n")
327
-
328
- self.stdout.write("\n")
329
-
330
- if arg == "all":
331
- # Display all commands with their details
332
- command_details = self.cmd_reg.get_all_commands()
333
- for name, (doc, args) in sorted(command_details.items()):
334
- get_info(name, doc, args)
335
-
336
- elif arg:
337
- # Display help for a specific command
338
- command = self.cmd_reg.get(arg)
339
- if command:
340
- doc = command.func.__doc__ or "No help available."
341
- args = command.sig.parameters
342
- get_info(arg, doc, args)
343
- else:
344
- self.stdout.write(f"\nUnknown command: '{arg}'\n")
345
- self.stdout.write("Type 'help' to see available commands.\n")
346
- else:
347
- # Display general help information
348
- self.stdout.write("\n")
349
- self.stdout.write("JAC PROGRAMMING LANGUAGE COMMAND LINE INTERFACE\n")
350
- self.stdout.write("=" * 50 + "\n\n")
351
-
352
- self.stdout.write("AVAILABLE COMMANDS:\n")
353
-
354
- # Get all command names and sort them alphabetically
355
- command_names = sorted(self.cmd_reg.registry.keys())
356
-
357
- # Get brief descriptions for each command
358
- command_descriptions: dict[str, str] = {}
359
- for name in command_names:
360
- command = self.cmd_reg.get(name)
361
- if command and command.func.__doc__:
362
- # Extract the first line of the docstring as a brief description
363
- brief = command.func.__doc__.split("\n")[0].strip()
364
- command_descriptions[name] = brief
365
- else:
366
- command_descriptions[name] = "No description available"
367
-
368
- # Display commands with their brief descriptions
369
- for name in command_names:
370
- self.stdout.write(
371
- f" {name.ljust(15)} - {command_descriptions[name]}\n"
372
- )
373
-
374
- self.stdout.write("\nFor detailed information on a specific command:\n")
375
- self.stdout.write(" help <command>\n")
376
-
377
- self.stdout.write("\nTo see detailed information for all commands:\n")
378
- self.stdout.write(" help all\n")
379
-
380
- self.stdout.write("\nTo exit the Jac CLI:\n")
381
- self.stdout.write(" exit\n\n")
242
+ # Shell mode removed; interactive cmd-based shell is no longer supported.
@@ -1002,7 +1002,8 @@ class PyastGenPass(UniPass):
1002
1002
  def exit_func_signature(self, node: uni.FuncSignature) -> None:
1003
1003
  params = (
1004
1004
  [self.sync(ast3.arg(arg="self", annotation=None))]
1005
- if (abl := node.find_parent_of_type(uni.Ability))
1005
+ if (abl := node.parent)
1006
+ and isinstance(abl, uni.Ability)
1006
1007
  and abl.is_method
1007
1008
  and not node.is_static
1008
1009
  and not node.is_in_py_class
@@ -2123,6 +2124,10 @@ class PyastGenPass(UniPass):
2123
2124
  ]
2124
2125
 
2125
2126
  def exit_lambda_expr(self, node: uni.LambdaExpr) -> None:
2127
+ # Python lambda expressions don't support type annotations
2128
+ if node.signature:
2129
+ self._remove_lambda_param_annotations(node.signature)
2130
+
2126
2131
  node.gen.py_ast = [
2127
2132
  self.sync(
2128
2133
  ast3.Lambda(
@@ -2144,6 +2149,11 @@ class PyastGenPass(UniPass):
2144
2149
  )
2145
2150
  ]
2146
2151
 
2152
+ def _remove_lambda_param_annotations(self, signature: uni.FuncSignature) -> None:
2153
+ for param in signature.params:
2154
+ if param.gen.py_ast and isinstance(param.gen.py_ast[0], ast3.arg):
2155
+ param.gen.py_ast[0].annotation = None
2156
+
2147
2157
  def exit_unary_expr(self, node: uni.UnaryExpr) -> None:
2148
2158
  op_cls = UNARY_OP_MAP.get(node.op.name)
2149
2159
  if op_cls:
@@ -2400,7 +2410,7 @@ class PyastGenPass(UniPass):
2400
2410
  self.sync(
2401
2411
  ast3.Attribute(
2402
2412
  value=cast(ast3.expr, node.target.gen.py_ast[0]),
2403
- attr=(node.right.sym_name),
2413
+ attr=node.right.sym_name,
2404
2414
  ctx=cast(ast3.expr_context, node.right.py_ctx_func()),
2405
2415
  )
2406
2416
  )
@@ -2997,7 +3007,7 @@ class PyastGenPass(UniPass):
2997
3007
  node.gen.py_ast = [self.sync(op_cls())]
2998
3008
 
2999
3009
  def exit_name(self, node: uni.Name) -> None:
3000
- name = node.sym_name[2:] if node.sym_name.startswith("<>") else node.sym_name
3010
+ name = node.sym_name
3001
3011
  node.gen.py_ast = [self.sync(ast3.Name(id=name, ctx=node.py_ctx_func()))]
3002
3012
 
3003
3013
  def exit_float(self, node: uni.Float) -> None:
@@ -141,17 +141,17 @@ class PyastBuildPass(Transform[uni.PythonModuleAst, uni.Module]):
141
141
 
142
142
  reserved_keywords = [v for _, v in TOKEN_MAP.items()]
143
143
 
144
- value = node.name if node.name not in reserved_keywords else f"<>{node.name}"
145
144
  name = uni.Name(
146
145
  orig_src=self.orig_src,
147
146
  name=Tok.NAME,
148
- value=value,
147
+ value=node.name,
149
148
  line=node.lineno,
150
149
  end_line=node.end_lineno if node.end_lineno else node.lineno,
151
150
  col_start=node.col_offset,
152
151
  col_end=node.col_offset + len(node.name),
153
152
  pos_start=0,
154
153
  pos_end=0,
154
+ is_kwesc=(node.name in reserved_keywords),
155
155
  )
156
156
  body = [self.convert(i) for i in node.body]
157
157
  valid = [i for i in body if isinstance(i, (uni.CodeBlockStmt))]
@@ -285,8 +285,8 @@ class PyastBuildPass(Transform[uni.PythonModuleAst, uni.Module]):
285
285
  and body_stmt.signature.params
286
286
  ):
287
287
  for param in body_stmt.signature.params:
288
- if param.name.value == "self":
289
- param.type_tag = uni.SubTag[uni.Expr](name, kid=[name])
288
+ if param.name.value == "self" and param.type_tag:
289
+ param.type_tag.tag.value = name.value
290
290
  doc = (
291
291
  body[0].expr
292
292
  if isinstance(body[0], uni.ExprStmt)
@@ -748,17 +748,14 @@ class PyastBuildPass(Transform[uni.PythonModuleAst, uni.Module]):
748
748
  attribute = uni.Name(
749
749
  orig_src=self.orig_src,
750
750
  name=Tok.NAME,
751
- value=(
752
- ("<>" + node.attr)
753
- if node.attr == "init"
754
- else "init" if node.attr == "__init__" else node.attr
755
- ),
751
+ value="init" if node.attr == "__init__" else node.attr,
756
752
  line=node.lineno,
757
753
  end_line=node.end_lineno if node.end_lineno else node.lineno,
758
754
  col_start=node.col_offset,
759
755
  col_end=node.col_offset + len(node.attr),
760
756
  pos_start=0,
761
757
  pos_end=0,
758
+ is_kwesc=node.attr == "init",
762
759
  )
763
760
  if isinstance(value, uni.Expr):
764
761
  return uni.AtomTrailer(
@@ -1680,18 +1677,17 @@ class PyastBuildPass(Transform[uni.PythonModuleAst, uni.Module]):
1680
1677
  for _, v in TOKEN_MAP.items()
1681
1678
  if v not in ["float", "int", "str", "bool", "self"]
1682
1679
  ]
1683
-
1684
- value = node.id if node.id not in reserved_keywords else f"<>{node.id}"
1685
1680
  ret = uni.Name(
1686
1681
  orig_src=self.orig_src,
1687
1682
  name=Tok.NAME,
1688
- value=value,
1683
+ value=node.id,
1689
1684
  line=node.lineno,
1690
1685
  end_line=node.end_lineno if node.end_lineno else node.lineno,
1691
1686
  col_start=node.col_offset,
1692
1687
  col_end=node.col_offset + len(node.id),
1693
1688
  pos_start=0,
1694
1689
  pos_end=0,
1690
+ is_kwesc=(node.id in reserved_keywords),
1695
1691
  )
1696
1692
  return ret
1697
1693
 
@@ -1735,18 +1731,18 @@ class PyastBuildPass(Transform[uni.PythonModuleAst, uni.Module]):
1735
1731
 
1736
1732
  names: list[uni.NameAtom] = []
1737
1733
  for name in node.names:
1738
- value = name if name not in reserved_keywords else f"<>{name}"
1739
1734
  names.append(
1740
1735
  uni.Name(
1741
1736
  orig_src=self.orig_src,
1742
1737
  name=Tok.NAME,
1743
- value=value,
1738
+ value=name,
1744
1739
  line=node.lineno,
1745
1740
  end_line=node.end_lineno if node.end_lineno else node.lineno,
1746
1741
  col_start=node.col_offset,
1747
1742
  col_end=node.col_offset + len(name),
1748
1743
  pos_start=0,
1749
1744
  pos_end=0,
1745
+ is_kwesc=(name in reserved_keywords),
1750
1746
  )
1751
1747
  )
1752
1748
  return uni.NonLocalStmt(target=names, kid=names)
@@ -2056,17 +2052,17 @@ class PyastBuildPass(Transform[uni.PythonModuleAst, uni.Module]):
2056
2052
  if v not in ["float", "int", "str", "bool", "self"]
2057
2053
  ]
2058
2054
 
2059
- value = node.arg if node.arg not in reserved_keywords else f"<>{node.arg}"
2060
2055
  name = uni.Name(
2061
2056
  orig_src=self.orig_src,
2062
2057
  name=Tok.NAME,
2063
- value=value,
2058
+ value=node.arg,
2064
2059
  line=node.lineno,
2065
2060
  end_line=node.end_lineno if node.end_lineno else node.lineno,
2066
2061
  col_start=node.col_offset,
2067
2062
  col_end=node.col_offset + len(node.arg),
2068
2063
  pos_start=0,
2069
2064
  pos_end=0,
2065
+ is_kwesc=(node.arg in reserved_keywords),
2070
2066
  )
2071
2067
  ann_expr = (
2072
2068
  self.convert(node.annotation)
@@ -2343,19 +2339,17 @@ class PyastBuildPass(Transform[uni.PythonModuleAst, uni.Module]):
2343
2339
  from jaclang.compiler import TOKEN_MAP
2344
2340
 
2345
2341
  reserved_keywords = [v for _, v in TOKEN_MAP.items()]
2346
- arg_value = (
2347
- node.arg if node.arg not in reserved_keywords else f"<>{node.arg}"
2348
- )
2349
2342
  arg = uni.Name(
2350
2343
  orig_src=self.orig_src,
2351
2344
  name=Tok.NAME,
2352
- value=arg_value,
2345
+ value=node.arg,
2353
2346
  line=node.lineno,
2354
2347
  end_line=node.end_lineno if node.end_lineno else node.lineno,
2355
2348
  col_start=node.col_offset,
2356
2349
  col_end=node.col_offset + len(node.arg if node.arg else "_"),
2357
2350
  pos_start=0,
2358
2351
  pos_end=0,
2352
+ is_kwesc=(node.arg in reserved_keywords),
2359
2353
  )
2360
2354
  value = self.convert(node.value)
2361
2355
  if isinstance(value, uni.Expr):
@@ -0,0 +1,21 @@
1
+ node B {
2
+ def __mul__(other: B) -> int {
3
+ return 0;
4
+ }
5
+ }
6
+
7
+ node A {
8
+ def __add__(other: A) -> B {
9
+ return B();
10
+ }
11
+ }
12
+
13
+ with entry {
14
+ a: A = A();
15
+
16
+ r1: B = a + a; # <-- Ok
17
+ r2: A = a + a; # <-- Error
18
+
19
+ r3: int = (a+a) * B(); # <-- Ok
20
+ r4: str = (a+a) * B(); # <-- Error
21
+ }
@@ -0,0 +1,12 @@
1
+
2
+ node Foo {
3
+ has i: int = 0;
4
+ }
5
+
6
+ with entry {
7
+ cls = Foo;
8
+ inst = cls(); # <-- Call expression target of class type
9
+
10
+ inst.i = 42; # <-- Ok
11
+ inst.i = 'str'; # <-- Error
12
+ }
@@ -0,0 +1,4 @@
1
+
2
+ with entry {
3
+ x = x;
4
+ }
@@ -0,0 +1,9 @@
1
+
2
+ def foo() -> int {
3
+ return 42;
4
+ }
5
+
6
+ with entry {
7
+ i: int = foo(); # <-- Ok
8
+ s: str = foo(); # <-- Error
9
+ }
@@ -0,0 +1,13 @@
1
+ import from scipy { stats, optimize }
2
+ import from utils.fake_helpers { helper_func }
3
+ import from non_existent_module { foo }
4
+ import nonexistent_module as nm;
5
+
6
+
7
+ with entry {
8
+ a: int = stats.norm.cdf(0);
9
+ d: float = optimize.minimize_scalar(lambda x: int: x ** 2).fun;
10
+ result = helper_func();
11
+ b: int = foo();
12
+ c = nm.some_func();
13
+ }
@@ -0,0 +1,17 @@
1
+
2
+ node Bar {}
3
+
4
+ node Foo {
5
+ def __call__() -> Bar {
6
+ return Bar();
7
+ }
8
+ }
9
+
10
+ def fn() -> Foo {
11
+ return Foo();
12
+ }
13
+
14
+ with entry{
15
+ b: Bar = fn()(); # <-- Ok
16
+ f: Foo = fn()(); # <-- Error
17
+ }
@@ -0,0 +1,8 @@
1
+
2
+
3
+ import jaclang.compiler.unitree as uni;
4
+
5
+
6
+ with entry{
7
+ a:int = uni.Module; # <-- Error
8
+ }
@@ -81,6 +81,80 @@ class TypeCheckerPassTests(TestCase):
81
81
  ^^^^^^^^^^^^^^
82
82
  """, program.errors_had[0].pretty_print())
83
83
 
84
+ def test_call_expr(self) -> None:
85
+ path = self.fixture_abs_path("checker_expr_call.jac")
86
+ program = JacProgram()
87
+ mod = program.compile(path)
88
+ TypeCheckPass(ir_in=mod, prog=program)
89
+ self.assertEqual(len(program.errors_had), 1)
90
+ self._assert_error_pretty_found("""
91
+ s: str = foo();
92
+ ^^^^^^^^^^^^^^
93
+ """, program.errors_had[0].pretty_print())
94
+
95
+ def test_call_expr_magic(self) -> None:
96
+ path = self.fixture_abs_path("checker_magic_call.jac")
97
+ program = JacProgram()
98
+ mod = program.compile(path)
99
+ TypeCheckPass(ir_in=mod, prog=program)
100
+ self.assertEqual(len(program.errors_had), 1)
101
+ self._assert_error_pretty_found("""
102
+ b: Bar = fn()(); # <-- Ok
103
+ f: Foo = fn()(); # <-- Error
104
+ ^^^^^^^^^^^^^^^^
105
+ """, program.errors_had[0].pretty_print())
106
+
107
+ def test_binary_op(self) -> None:
108
+ program = JacProgram()
109
+ mod = program.compile(self.fixture_abs_path("checker_binary_op.jac"))
110
+ TypeCheckPass(ir_in=mod, prog=program)
111
+ self.assertEqual(len(program.errors_had), 2)
112
+ self._assert_error_pretty_found("""
113
+ r2: A = a + a; # <-- Error
114
+ ^^^^^^^^^^^^^
115
+ """, program.errors_had[0].pretty_print())
116
+ self._assert_error_pretty_found("""
117
+ r4: str = (a+a) * B(); # <-- Error
118
+ ^^^^^^^^^^^^^^^^^^^^^
119
+ """, program.errors_had[1].pretty_print())
120
+
121
+ def test_checker_call_expr_class(self) -> None:
122
+ path = self.fixture_abs_path("checker_call_expr_class.jac")
123
+ program = JacProgram()
124
+ mod = program.compile(path)
125
+ TypeCheckPass(ir_in=mod, prog=program)
126
+ self.assertEqual(len(program.errors_had), 1)
127
+ self._assert_error_pretty_found("""
128
+ inst.i = 'str'; # <-- Error
129
+ ^^^^^^^^^^^^^^
130
+ """, program.errors_had[0].pretty_print())
131
+
132
+ def test_checker_mod_path(self) -> None:
133
+ path = self.fixture_abs_path("checker_mod_path.jac")
134
+ program = JacProgram()
135
+ mod = program.compile(path)
136
+ TypeCheckPass(ir_in=mod, prog=program)
137
+ self.assertEqual(len(program.errors_had), 1)
138
+ self._assert_error_pretty_found("""
139
+ a:int = uni.Module; # <-- Error
140
+ ^^^^^^^^^^^^^^
141
+ """, program.errors_had[0].pretty_print())
142
+
143
+ def test_checker_import_missing_module(self) -> None:
144
+ path = self.fixture_abs_path("checker_import_missing_module.jac")
145
+ program = JacProgram()
146
+ mod = program.compile(path)
147
+ TypeCheckPass(ir_in=mod, prog=program)
148
+ self.assertEqual(len(program.errors_had), 0)
149
+
150
+ def test_cyclic_symbol(self) -> None:
151
+ path = self.fixture_abs_path("checker_cyclic_symbol.jac")
152
+ program = JacProgram()
153
+ mod = program.compile(path)
154
+ # This will result in a stack overflow if not handled properly.
155
+ # So the fact that it has 0 errors means it passed.
156
+ TypeCheckPass(ir_in=mod, prog=program)
157
+ self.assertEqual(len(program.errors_had), 0)
84
158
 
85
159
  def _assert_error_pretty_found(self, needle: str, haystack: str) -> None:
86
160
  for line in [line.strip() for line in needle.splitlines() if line.strip()]:
@@ -36,16 +36,28 @@ class TypeCheckPass(UniPass):
36
36
  # Cache the builtins module once it parsed.
37
37
  _BUILTINS_MODULE: uni.Module | None = None
38
38
 
39
+ # REVIEW: Making the evaluator a static (singleton) variable to make sure only one
40
+ # instance is used across mulitple compilation units. This can also be attached to an
41
+ # attribute of JacProgram, however the evaluator is a temproary object that we dont
42
+ # want bound to the program for long term, Also the program is the one that will be
43
+ # dumped in the compiled bundle.
44
+ _EVALUATOR: TypeEvaluator | None = None
45
+
39
46
  def before_pass(self) -> None:
40
47
  """Initialize the checker pass."""
41
48
  self._load_builtins_stub_module()
42
49
  self._insert_builtin_symbols()
43
50
 
44
- assert TypeCheckPass._BUILTINS_MODULE is not None
45
- self.evaluator = TypeEvaluator(
46
- builtins_module=TypeCheckPass._BUILTINS_MODULE,
47
- program=self.prog,
48
- )
51
+ @property
52
+ def evaluator(self) -> TypeEvaluator:
53
+ """Return the type evaluator."""
54
+ if TypeCheckPass._EVALUATOR is None:
55
+ assert TypeCheckPass._BUILTINS_MODULE is not None
56
+ TypeCheckPass._EVALUATOR = TypeEvaluator(
57
+ builtins_module=TypeCheckPass._BUILTINS_MODULE,
58
+ program=self.prog,
59
+ )
60
+ return TypeCheckPass._EVALUATOR
49
61
 
50
62
  # --------------------------------------------------------------------------
51
63
  # Internal helper functions
@@ -126,3 +138,10 @@ class TypeCheckPass(UniPass):
126
138
  def exit_atom_trailer(self, node: uni.AtomTrailer) -> None:
127
139
  """Handle the atom trailer node."""
128
140
  self.evaluator.get_type_of_expression(node)
141
+
142
+ def exit_func_call(self, node: uni.FuncCall) -> None:
143
+ """Handle the function call node."""
144
+ # TODO:
145
+ # 1. Function Existence & Callable Validation
146
+ # 2. Argument Matching(count, types, names)
147
+ self.evaluator.get_type_of_expression(node)