jaclang 0.8.7__py3-none-any.whl → 0.8.8__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 (89) hide show
  1. jaclang/cli/cli.py +13 -27
  2. jaclang/cli/cmdreg.py +44 -0
  3. jaclang/compiler/constant.py +0 -1
  4. jaclang/compiler/jac.lark +3 -6
  5. jaclang/compiler/larkparse/jac_parser.py +2 -2
  6. jaclang/compiler/parser.py +213 -34
  7. jaclang/compiler/passes/main/__init__.py +2 -4
  8. jaclang/compiler/passes/main/def_use_pass.py +0 -4
  9. jaclang/compiler/passes/main/predynamo_pass.py +221 -0
  10. jaclang/compiler/passes/main/pyast_gen_pass.py +70 -52
  11. jaclang/compiler/passes/main/pyast_load_pass.py +52 -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 +191 -0
  27. jaclang/compiler/passes/main/tests/test_predynamo_pass.py +57 -0
  28. jaclang/compiler/passes/main/type_checker_pass.py +29 -73
  29. jaclang/compiler/passes/tool/doc_ir_gen_pass.py +204 -44
  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/tagbreak.jac +171 -11
  34. jaclang/compiler/passes/transform.py +12 -8
  35. jaclang/compiler/program.py +14 -6
  36. jaclang/compiler/tests/fixtures/jac_import_py_files.py +4 -0
  37. jaclang/compiler/tests/fixtures/jac_module.jac +3 -0
  38. jaclang/compiler/tests/fixtures/multiple_syntax_errors.jac +10 -0
  39. jaclang/compiler/tests/fixtures/python_module.py +1 -0
  40. jaclang/compiler/tests/test_importer.py +39 -0
  41. jaclang/compiler/tests/test_parser.py +49 -0
  42. jaclang/compiler/type_system/type_evaluator.py +351 -67
  43. jaclang/compiler/type_system/type_utils.py +246 -0
  44. jaclang/compiler/type_system/types.py +58 -2
  45. jaclang/compiler/unitree.py +79 -94
  46. jaclang/langserve/engine.jac +138 -159
  47. jaclang/langserve/server.jac +25 -1
  48. jaclang/langserve/tests/fixtures/circle.jac +3 -3
  49. jaclang/langserve/tests/fixtures/circle_err.jac +3 -3
  50. jaclang/langserve/tests/fixtures/circle_pure.test.jac +3 -3
  51. jaclang/langserve/tests/fixtures/completion_test_err.jac +10 -0
  52. jaclang/langserve/tests/server_test/circle_template.jac +80 -0
  53. jaclang/langserve/tests/server_test/glob_template.jac +4 -0
  54. jaclang/langserve/tests/server_test/test_lang_serve.py +154 -309
  55. jaclang/langserve/tests/server_test/utils.py +153 -116
  56. jaclang/langserve/tests/test_server.py +21 -84
  57. jaclang/langserve/utils.jac +12 -15
  58. jaclang/runtimelib/machine.py +7 -0
  59. jaclang/runtimelib/meta_importer.py +27 -1
  60. jaclang/runtimelib/tests/fixtures/custom_access_validation.jac +1 -1
  61. jaclang/runtimelib/tests/fixtures/savable_object.jac +2 -2
  62. jaclang/settings.py +18 -14
  63. jaclang/tests/fixtures/abc_check.jac +3 -3
  64. jaclang/tests/fixtures/arch_rel_import_creation.jac +12 -12
  65. jaclang/tests/fixtures/chandra_bugs2.jac +3 -3
  66. jaclang/tests/fixtures/create_dynamic_archetype.jac +13 -13
  67. jaclang/tests/fixtures/maxfail_run_test.jac +4 -4
  68. jaclang/tests/fixtures/params/param_syntax_err.jac +9 -0
  69. jaclang/tests/fixtures/params/test_complex_params.jac +42 -0
  70. jaclang/tests/fixtures/params/test_failing_kwonly.jac +207 -0
  71. jaclang/tests/fixtures/params/test_failing_posonly.jac +116 -0
  72. jaclang/tests/fixtures/params/test_failing_varargs.jac +300 -0
  73. jaclang/tests/fixtures/params/test_kwonly_params.jac +29 -0
  74. jaclang/tests/fixtures/py2jac_params.py +8 -0
  75. jaclang/tests/fixtures/run_test.jac +4 -4
  76. jaclang/tests/test_cli.py +37 -1
  77. jaclang/tests/test_language.py +74 -16
  78. jaclang/utils/helpers.py +47 -2
  79. jaclang/utils/module_resolver.py +10 -0
  80. jaclang/utils/test.py +8 -0
  81. jaclang/utils/treeprinter.py +0 -18
  82. {jaclang-0.8.7.dist-info → jaclang-0.8.8.dist-info}/METADATA +1 -2
  83. {jaclang-0.8.7.dist-info → jaclang-0.8.8.dist-info}/RECORD +85 -60
  84. {jaclang-0.8.7.dist-info → jaclang-0.8.8.dist-info}/WHEEL +1 -1
  85. jaclang/compiler/passes/main/inheritance_pass.py +0 -131
  86. jaclang/langserve/dev_engine.jac +0 -645
  87. jaclang/langserve/dev_server.jac +0 -201
  88. jaclang/langserve/tests/server_test/code_test.py +0 -0
  89. {jaclang-0.8.7.dist-info → jaclang-0.8.8.dist-info}/entry_points.txt +0 -0
@@ -220,6 +220,8 @@ class DocIRGenPass(UniPass):
220
220
  parts.pop()
221
221
  parts.append(i.gen.doc_ir)
222
222
  parts.append(self.space())
223
+ elif not in_body and isinstance(i, uni.Token) and i.name == Tok.DECOR_OP:
224
+ parts.append(i.gen.doc_ir)
223
225
  else:
224
226
  parts.append(i.gen.doc_ir)
225
227
  parts.append(self.space())
@@ -257,6 +259,8 @@ class DocIRGenPass(UniPass):
257
259
  parts.pop()
258
260
  parts.append(i.gen.doc_ir)
259
261
  parts.append(self.space())
262
+ elif not in_body and isinstance(i, uni.Token) and i.name == Tok.DECOR_OP:
263
+ parts.append(i.gen.doc_ir)
260
264
  else:
261
265
  parts.append(i.gen.doc_ir)
262
266
  parts.append(self.space())
@@ -275,8 +279,17 @@ class DocIRGenPass(UniPass):
275
279
  elif isinstance(i, uni.Token) and i.name == Tok.RPAREN and node.params:
276
280
  in_params = False
277
281
  has_parens = True
282
+ if isinstance(indent_parts[-1], doc.Line):
283
+ indent_parts.pop()
278
284
  parts.append(
279
- self.indent(self.concat([self.tight_line(), *indent_parts]))
285
+ self.indent(
286
+ self.concat(
287
+ [
288
+ self.tight_line(),
289
+ self.group(self.concat([*indent_parts])),
290
+ ]
291
+ )
292
+ )
280
293
  )
281
294
  parts.append(self.tight_line())
282
295
  parts.append(i.gen.doc_ir)
@@ -346,14 +359,14 @@ class DocIRGenPass(UniPass):
346
359
  lhs_parts.append(self.space())
347
360
 
348
361
  if eq_tok is not None:
349
- rhs_concat = self.concat(rhs_parts)
362
+ rhs_concat = self.group(self.concat(rhs_parts))
350
363
  node.gen.doc_ir = self.group(
351
364
  self.concat(
352
365
  [
353
366
  *lhs_parts,
354
367
  self.space(),
355
368
  eq_tok,
356
- self.indent(self.concat([self.line(), rhs_concat])),
369
+ self.concat([self.space(), rhs_concat]),
357
370
  ]
358
371
  )
359
372
  )
@@ -375,6 +388,20 @@ class DocIRGenPass(UniPass):
375
388
  parts.pop()
376
389
  parts.append(i.gen.doc_ir)
377
390
  parts.append(self.space())
391
+ elif i == node.condition and isinstance(i, uni.BoolExpr):
392
+ cond_str = i.gen.doc_ir
393
+ flat = self.concat([cond_str, self.space()])
394
+ broken = self.group(
395
+ self.concat(
396
+ [
397
+ self.text("("),
398
+ self.indent(self.concat([self.line(), cond_str])),
399
+ self.line(),
400
+ self.text(")"),
401
+ ]
402
+ )
403
+ )
404
+ parts.append(self.if_break(broken, flat))
378
405
  else:
379
406
  parts.append(i.gen.doc_ir)
380
407
  parts.append(self.space())
@@ -397,6 +424,21 @@ class DocIRGenPass(UniPass):
397
424
  parts.pop()
398
425
  parts.append(i.gen.doc_ir)
399
426
  parts.append(self.space())
427
+ elif i == node.condition and isinstance(i, uni.BoolExpr):
428
+ cond_str = i.gen.doc_ir
429
+ flat = self.concat([cond_str, self.space()])
430
+ broken = self.group(
431
+ self.concat(
432
+ [
433
+ self.text("("),
434
+ self.indent(self.concat([self.line(), cond_str])),
435
+ self.line(),
436
+ self.text(")"),
437
+ self.space(),
438
+ ]
439
+ )
440
+ )
441
+ parts.append(self.if_break(broken, flat))
400
442
  else:
401
443
  parts.append(i.gen.doc_ir)
402
444
  parts.append(self.space())
@@ -471,8 +513,18 @@ class DocIRGenPass(UniPass):
471
513
  parts.append(i.gen.doc_ir)
472
514
  elif isinstance(i, uni.Token) and i.name == Tok.RPAREN and node.params:
473
515
  in_params = False
516
+
517
+ if isinstance(indent_parts[-1], doc.Line):
518
+ indent_parts.pop()
474
519
  parts.append(
475
- self.indent(self.concat([self.tight_line(), *indent_parts]))
520
+ self.indent(
521
+ self.concat(
522
+ [
523
+ self.tight_line(),
524
+ self.group(self.concat([*indent_parts])),
525
+ ]
526
+ )
527
+ )
476
528
  )
477
529
  parts.append(self.tight_line())
478
530
  parts.append(i.gen.doc_ir)
@@ -510,12 +562,16 @@ class DocIRGenPass(UniPass):
510
562
  if isinstance(i, uni.Token) and i.name == Tok.COMMA:
511
563
  parts.append(i.gen.doc_ir)
512
564
  parts.append(self.hard_line())
513
- elif isinstance(i, uni.Token) and i.name == Tok.LSQUARE:
514
- parts.append(self.hard_line())
515
- parts.append(i.gen.doc_ir)
516
565
  else:
517
566
  parts.append(i.gen.doc_ir)
518
- broke = self.concat(parts)
567
+ broke = self.concat(
568
+ [
569
+ parts[0],
570
+ self.indent(self.concat([self.hard_line(), *parts[1:-1]])),
571
+ self.hard_line(),
572
+ parts[-1],
573
+ ]
574
+ )
519
575
  node.gen.doc_ir = self.group(self.if_break(broke, not_broke))
520
576
 
521
577
  def exit_dict_val(self, node: uni.DictVal) -> None:
@@ -699,12 +755,16 @@ class DocIRGenPass(UniPass):
699
755
  if isinstance(i, uni.Token) and i.name == Tok.COMMA:
700
756
  parts.append(i.gen.doc_ir)
701
757
  parts.append(self.hard_line())
702
- elif isinstance(i, uni.Token) and i.name == Tok.LPAREN:
703
- parts.append(self.hard_line())
704
- parts.append(i.gen.doc_ir)
705
758
  else:
706
759
  parts.append(i.gen.doc_ir)
707
- broke = self.concat(parts)
760
+ broke = self.concat(
761
+ [
762
+ parts[0],
763
+ self.indent(self.concat([self.hard_line(), *parts[1:-1]])),
764
+ self.hard_line(),
765
+ parts[-1],
766
+ ]
767
+ )
708
768
  node.gen.doc_ir = self.group(self.if_break(broke, not_broke))
709
769
 
710
770
  def exit_multi_string(self, node: uni.MultiString) -> None:
@@ -748,17 +808,34 @@ class DocIRGenPass(UniPass):
748
808
  """Generate DocIR for list comprehensions."""
749
809
  parts: list[doc.DocType] = []
750
810
  for i in node.kid:
751
- parts.append(i.gen.doc_ir)
811
+ if isinstance(i, uni.InnerCompr):
812
+ parts.append(self.group(self.concat([self.tight_line(), i.gen.doc_ir])))
813
+ else:
814
+ parts.append(i.gen.doc_ir)
752
815
  parts.append(self.space())
753
816
  parts.pop()
754
- node.gen.doc_ir = self.group(self.concat(parts))
817
+ node.gen.doc_ir = self.group(
818
+ self.concat(
819
+ [
820
+ parts[0],
821
+ self.indent(self.concat([self.tight_line(), *parts[2:-2]])),
822
+ self.tight_line(),
823
+ parts[-1],
824
+ ]
825
+ )
826
+ )
755
827
 
756
828
  def exit_inner_compr(self, node: uni.InnerCompr) -> None:
757
829
  """Generate DocIR for inner comprehension clauses."""
758
830
  parts: list[doc.DocType] = []
759
831
  for i in node.kid:
760
- parts.append(i.gen.doc_ir)
761
- parts.append(self.space())
832
+ if isinstance(i, uni.Token) and i.name == Tok.KW_IF:
833
+ parts.append(self.hard_line())
834
+ parts.append(i.gen.doc_ir)
835
+ parts.append(self.space())
836
+ else:
837
+ parts.append(i.gen.doc_ir)
838
+ parts.append(self.space())
762
839
  parts.pop()
763
840
  node.gen.doc_ir = self.group(self.concat(parts))
764
841
 
@@ -772,6 +849,7 @@ class DocIRGenPass(UniPass):
772
849
  def exit_if_else_expr(self, node: uni.IfElseExpr) -> None:
773
850
  """Generate DocIR for conditional expressions."""
774
851
  parts: list[doc.DocType] = []
852
+ need_parens = not isinstance(node.parent, uni.AtomUnit)
775
853
 
776
854
  for i in node.kid:
777
855
  if isinstance(i, uni.Expr):
@@ -782,21 +860,60 @@ class DocIRGenPass(UniPass):
782
860
  parts.append(self.space())
783
861
  else:
784
862
  parts.append(i.gen.doc_ir)
785
- parts.append(self.space())
863
+ parts.append(self.line())
786
864
  parts.pop()
787
- node.gen.doc_ir = self.group(self.concat(parts))
865
+
866
+ flat = self.group(self.concat(parts))
867
+ parens = self.group(
868
+ self.concat(
869
+ [
870
+ self.text("("),
871
+ self.indent(self.concat([self.tight_line(), flat])),
872
+ self.tight_line(),
873
+ self.text(")"),
874
+ ]
875
+ )
876
+ )
877
+ node.gen.doc_ir = flat
878
+
879
+ if need_parens:
880
+ if isinstance(node.parent, uni.Assignment):
881
+ node.gen.doc_ir = self.if_break(
882
+ break_contents=parens, flat_contents=flat
883
+ )
884
+ else:
885
+ node.gen.doc_ir = parens
788
886
 
789
887
  def exit_bool_expr(self, node: uni.BoolExpr) -> None:
790
888
  """Generate DocIR for boolean expressions (and/or)."""
889
+ exprs: list[uni.UniNode] = []
791
890
  parts: list[doc.DocType] = []
792
- for i in node.kid:
793
- if isinstance(i, uni.Token):
794
- parts.append(i.gen.doc_ir)
795
- parts.append(self.space())
891
+
892
+ def __flatten_bool_expr(expr: uni.Expr) -> list[uni.UniNode]:
893
+ if isinstance(expr, uni.BoolExpr):
894
+ out: list[uni.UniNode] = []
895
+ for val in expr.values:
896
+ out += __flatten_bool_expr(val)
897
+ out.append(expr.op)
898
+ out.pop()
899
+ return out
796
900
  else:
797
- parts.append(i.gen.doc_ir)
798
- parts.append(self.line()) # Potential break
799
- node.gen.doc_ir = self.group(self.concat(parts))
901
+ return [expr]
902
+
903
+ exprs = __flatten_bool_expr(node)
904
+ for i in range(0, len(exprs) - 1, 2):
905
+ (
906
+ expr,
907
+ op,
908
+ ) = (
909
+ exprs[i],
910
+ exprs[i + 1],
911
+ )
912
+ parts += [expr.gen.doc_ir, self.space(), op.gen.doc_ir, self.line()]
913
+ parts += [exprs[-1].gen.doc_ir, self.line()]
914
+ parts.pop()
915
+ flat = self.concat(parts)
916
+ node.gen.doc_ir = self.group(flat)
800
917
 
801
918
  def exit_unary_expr(self, node: uni.UnaryExpr) -> None:
802
919
  """Generate DocIR for unary expressions."""
@@ -860,27 +977,49 @@ class DocIRGenPass(UniPass):
860
977
  parts: list[doc.DocType] = []
861
978
  for i in node.kid:
862
979
  parts.append(i.gen.doc_ir)
863
- parts.append(self.space())
864
- parts.pop()
865
980
  node.gen.doc_ir = self.group(self.concat(parts))
866
981
 
867
982
  def exit_gen_compr(self, node: uni.GenCompr) -> None:
868
983
  """Generate DocIR for generator comprehensions."""
869
984
  parts: list[doc.DocType] = []
870
985
  for i in node.kid:
871
- parts.append(i.gen.doc_ir)
986
+ if isinstance(i, uni.InnerCompr):
987
+ parts.append(self.group(self.concat([self.tight_line(), i.gen.doc_ir])))
988
+ else:
989
+ parts.append(i.gen.doc_ir)
872
990
  parts.append(self.space())
873
991
  parts.pop()
874
- node.gen.doc_ir = self.group(self.concat(parts))
992
+ node.gen.doc_ir = self.group(
993
+ self.concat(
994
+ [
995
+ parts[0],
996
+ self.indent(self.concat([self.tight_line(), *parts[2:-2]])),
997
+ self.tight_line(),
998
+ parts[-1],
999
+ ]
1000
+ )
1001
+ )
875
1002
 
876
1003
  def exit_set_compr(self, node: uni.SetCompr) -> None:
877
1004
  """Generate DocIR for set comprehensions."""
878
1005
  parts: list[doc.DocType] = []
879
1006
  for i in node.kid:
880
- parts.append(i.gen.doc_ir)
1007
+ if isinstance(i, uni.InnerCompr):
1008
+ parts.append(self.group(self.concat([self.tight_line(), i.gen.doc_ir])))
1009
+ else:
1010
+ parts.append(i.gen.doc_ir)
881
1011
  parts.append(self.space())
882
1012
  parts.pop()
883
- node.gen.doc_ir = self.group(self.concat(parts))
1013
+ node.gen.doc_ir = self.group(
1014
+ self.concat(
1015
+ [
1016
+ parts[0],
1017
+ self.indent(self.concat([self.tight_line(), *parts[2:-2]])),
1018
+ self.tight_line(),
1019
+ parts[-1],
1020
+ ]
1021
+ )
1022
+ )
884
1023
 
885
1024
  def exit_dict_compr(self, node: uni.DictCompr) -> None:
886
1025
  """Generate DocIR for dictionary comprehensions."""
@@ -889,10 +1028,24 @@ class DocIRGenPass(UniPass):
889
1028
  if isinstance(i, uni.Token) and i.name in [Tok.STAR_POW, Tok.STAR_MUL]:
890
1029
  parts.append(i.gen.doc_ir)
891
1030
  else:
892
- parts.append(i.gen.doc_ir)
1031
+ if isinstance(i, uni.InnerCompr):
1032
+ parts.append(
1033
+ self.group(self.concat([self.tight_line(), i.gen.doc_ir]))
1034
+ )
1035
+ else:
1036
+ parts.append(i.gen.doc_ir)
893
1037
  parts.append(self.space())
894
1038
  parts.pop()
895
- node.gen.doc_ir = self.group(self.concat(parts))
1039
+ node.gen.doc_ir = self.group(
1040
+ self.concat(
1041
+ [
1042
+ parts[0],
1043
+ self.indent(self.concat([self.tight_line(), *parts[2:-2]])),
1044
+ self.tight_line(),
1045
+ parts[-1],
1046
+ ]
1047
+ )
1048
+ )
896
1049
 
897
1050
  def exit_k_w_pair(self, node: uni.KWPair) -> None:
898
1051
  """Generate DocIR for keyword arguments."""
@@ -1101,7 +1254,23 @@ class DocIRGenPass(UniPass):
1101
1254
  parts.append(self.space())
1102
1255
  prev_item = i
1103
1256
  parts.pop()
1104
- node.gen.doc_ir = self.group(self.concat(parts))
1257
+ broken = self.group(
1258
+ self.concat(
1259
+ [
1260
+ parts[0],
1261
+ self.indent(self.concat([self.tight_line(), *parts[1:-1]])),
1262
+ self.tight_line(),
1263
+ parts[-1],
1264
+ ]
1265
+ )
1266
+ )
1267
+ if isinstance(node.parent, uni.Assignment):
1268
+ node.gen.doc_ir = self.if_break(
1269
+ flat_contents=self.group(self.concat(parts[1:-1])),
1270
+ break_contents=broken,
1271
+ )
1272
+ else:
1273
+ node.gen.doc_ir = broken
1105
1274
 
1106
1275
  def exit_expr_as_item(self, node: uni.ExprAsItem) -> None:
1107
1276
  """Generate DocIR for expression as item nodes."""
@@ -1109,6 +1278,7 @@ class DocIRGenPass(UniPass):
1109
1278
  for i in node.kid:
1110
1279
  parts.append(i.gen.doc_ir)
1111
1280
  parts.append(self.space())
1281
+ parts.pop()
1112
1282
  node.gen.doc_ir = self.group(self.concat(parts))
1113
1283
 
1114
1284
  def exit_filter_compr(self, node: uni.FilterCompr) -> None:
@@ -1160,16 +1330,6 @@ class DocIRGenPass(UniPass):
1160
1330
  parts.append(self.space())
1161
1331
  node.gen.doc_ir = self.group(self.concat(parts))
1162
1332
 
1163
- def exit_check_stmt(self, node: uni.CheckStmt) -> None:
1164
- """Generate DocIR for check statements."""
1165
- parts: list[doc.DocType] = []
1166
- for i in node.kid:
1167
- if isinstance(i, uni.Token) and i.name == Tok.SEMI:
1168
- parts.pop()
1169
- parts.append(i.gen.doc_ir)
1170
- parts.append(self.space())
1171
- node.gen.doc_ir = self.group(self.concat(parts))
1172
-
1173
1333
  def exit_match_stmt(self, node: uni.MatchStmt) -> None:
1174
1334
  """Generate DocIR for match statements."""
1175
1335
  parts: list[doc.DocType] = []
@@ -3,7 +3,8 @@
3
3
  This is a pass for formatting Jac code.
4
4
  """
5
5
 
6
- from typing import Optional
6
+ from collections import deque
7
+ from typing import Deque, Optional, Tuple
7
8
 
8
9
  import jaclang.compiler.passes.tool.doc_ir as doc
9
10
  import jaclang.compiler.unitree as uni
@@ -19,6 +20,83 @@ class JacFormatPass(Transform[uni.Module, uni.Module]):
19
20
  self.indent_size = 4
20
21
  self.MAX_LINE_LENGTH = settings.max_line_length
21
22
 
23
+ def _probe_fits(
24
+ self,
25
+ node: doc.DocType,
26
+ indent_level: int,
27
+ width_remaining: int,
28
+ *,
29
+ max_steps: int = 2000,
30
+ ) -> bool:
31
+ """
32
+ Check if flat can be used early.
33
+
34
+ returns True if `node` could be printed *flat* on the current line within
35
+ `width_remaining` columns at `indent_level`.
36
+ Stops early on overflow or hard/literal lines.
37
+ """
38
+ # Worklist holds (node, indent_level). We only ever push FLAT in a probe.
39
+ work: Deque[Tuple[object, int]] = deque()
40
+ work.append((node, indent_level))
41
+ steps = 0
42
+ remaining = width_remaining
43
+
44
+ while work:
45
+ if steps >= max_steps:
46
+ # Safety cutoff: if it's *that* complex, assume it doesn't fit.
47
+ return False
48
+ steps += 1
49
+
50
+ cur, lvl = work.pop()
51
+
52
+ if isinstance(cur, doc.Text):
53
+ remaining -= len(cur.text)
54
+ if remaining < 0:
55
+ return False
56
+
57
+ elif isinstance(cur, doc.Line):
58
+ if cur.hard or cur.literal:
59
+ # Any *real* newline (hard or literal) in FLAT means "doesn't fit"
60
+ return False
61
+ if cur.tight:
62
+ # tight softline disappears in flat mode
63
+ continue
64
+ # regular soft line becomes a single space in flat mode
65
+ remaining -= 1
66
+ if remaining < 0:
67
+ return False
68
+
69
+ # --- Structural nodes (walk children in LIFO) ---
70
+ elif isinstance(cur, doc.Concat):
71
+ # push reversed so we process left-to-right as work is a stack
72
+ for p in reversed(cur.parts):
73
+ work.append((p, lvl))
74
+
75
+ elif isinstance(cur, doc.Group):
76
+ # Probe is always FLAT for groups.
77
+ work.append((cur.contents, lvl))
78
+
79
+ elif isinstance(cur, doc.Indent):
80
+ # In flat mode, indentation has no effect until a newline; keep lvl in case
81
+ # children contain Lines (which would have already returned False).
82
+ work.append((cur.contents, lvl + 1))
83
+
84
+ elif isinstance(cur, doc.Align):
85
+ # In flat mode, alignment doesn’t change width immediately (no newline),
86
+ # but we carry its virtual indent so nested (illegal) Line would be caught.
87
+ align_spaces = cur.n if cur.n is not None else self.indent_size
88
+ extra_levels = align_spaces // self.indent_size
89
+ work.append((cur.contents, lvl + extra_levels))
90
+
91
+ elif isinstance(cur, doc.IfBreak):
92
+ # Flat branch while probing
93
+ work.append((cur.flat_contents, lvl))
94
+
95
+ else:
96
+ raise ValueError(f"Unknown DocType in probe: {type(cur)}")
97
+
98
+ return True
99
+
22
100
  def transform(self, ir_in: uni.Module) -> uni.Module:
23
101
  """After pass."""
24
102
  ir_in.gen.jac = self.format_doc_ir()
@@ -52,106 +130,78 @@ class JacFormatPass(Transform[uni.Module, uni.Module]):
52
130
  return " "
53
131
 
54
132
  elif isinstance(doc_node, doc.Group):
55
- # Try to print flat first. For this attempt, the group itself isn't forced to break.
56
- flat_contents_str = self.format_doc_ir(
57
- doc_node.contents, indent_level, width_remaining, is_broken=False
133
+ fits_flat = self._probe_fits(
134
+ doc_node.contents,
135
+ indent_level=indent_level,
136
+ width_remaining=width_remaining,
137
+ )
138
+ return self.format_doc_ir(
139
+ doc_node.contents,
140
+ indent_level,
141
+ width_remaining,
142
+ is_broken=not fits_flat,
58
143
  )
59
- if (
60
- "\n" not in flat_contents_str
61
- and len(flat_contents_str) <= width_remaining
62
- ):
63
- return flat_contents_str
64
- else:
65
- full_width_for_broken_content = self.MAX_LINE_LENGTH - (
66
- indent_level * self.indent_size
67
- )
68
- return self.format_doc_ir(
69
- doc_node.contents,
70
- indent_level,
71
- full_width_for_broken_content,
72
- is_broken=True,
73
- )
74
144
 
75
145
  elif isinstance(doc_node, doc.Indent):
76
146
  new_indent_level = indent_level + 1
77
-
78
- width_for_indented_content = self.MAX_LINE_LENGTH - (
79
- new_indent_level * self.indent_size
80
- )
81
147
  return self.format_doc_ir(
82
148
  doc_node.contents,
83
149
  new_indent_level,
84
- width_for_indented_content, # Budget for lines within indent
150
+ width_remaining, # width_for_indented_content # Budget for lines within indent
85
151
  is_broken, # is_broken state propagates
86
152
  )
87
153
 
88
154
  elif isinstance(doc_node, doc.Concat):
89
- result = ""
90
- # current_line_budget is the space left on the current line for the current part.
155
+ result: list[str] = []
91
156
  current_line_budget = width_remaining
92
157
 
93
158
  for part in doc_node.parts:
94
159
  part_str = self.format_doc_ir(
95
160
  part, indent_level, current_line_budget, is_broken
96
161
  )
97
- if part_str.startswith("\n"):
98
- result = result.rstrip(" ")
99
- result += part_str
162
+
163
+ # Trim trailing spaces when a newline begins next
164
+ if part_str.startswith("\n") and result and result[-1].endswith(" "):
165
+ result[-1] = result[-1].rstrip(" ")
166
+
167
+ result.append(part_str)
100
168
 
101
169
  if "\n" in part_str:
102
- # part_str created a newline. The next part starts on a new line.
103
- # Its budget is the full width available at this indent level.
104
- current_line_budget = self.MAX_LINE_LENGTH - (
105
- indent_level * self.indent_size
170
+ # After a newline, reset budget to full width at this indent.
171
+ last_line = part_str.splitlines()[-1]
172
+ full_budget = max(
173
+ 0, self.MAX_LINE_LENGTH - indent_level * self.indent_size
106
174
  )
107
- # Subtract what the *last line* of part_str consumed from this budget.
108
- # The characters on the last line after the indent string.
109
- indent_str_len = indent_level * self.indent_size
110
- last_line_of_part = part_str.splitlines()[-1]
111
-
112
- content_on_last_line = 0
113
- if last_line_of_part.startswith(" " * indent_str_len):
114
- content_on_last_line = len(last_line_of_part) - indent_str_len
115
- else: # It was a line not starting with the full indent (e.g. literal \n)
116
- content_on_last_line = len(last_line_of_part)
117
-
118
- current_line_budget -= content_on_last_line
175
+ # Compute how many chars are already on the last line (after indent).
176
+ indent_spaces = " " * (indent_level * self.indent_size)
177
+ if last_line.startswith(indent_spaces):
178
+ used = len(last_line) - len(indent_spaces)
179
+ else:
180
+ used = len(last_line)
181
+ current_line_budget = max(0, full_budget - used)
119
182
  else:
120
- # part_str stayed on the same line. Reduce budget for next part on this line.
121
- current_line_budget -= len(part_str)
183
+ current_line_budget = max(0, current_line_budget - len(part_str))
122
184
 
123
- if current_line_budget < 0: # Ensure budget isn't negative
124
- current_line_budget = 0
125
- return result
185
+ return "".join(result)
126
186
 
127
187
  elif isinstance(doc_node, doc.IfBreak):
128
- if is_broken:
129
- return self.format_doc_ir(
130
- doc_node.break_contents, indent_level, width_remaining, is_broken
131
- )
132
- else:
133
- return self.format_doc_ir(
134
- doc_node.flat_contents, indent_level, width_remaining, is_broken
135
- )
188
+ branch = doc_node.break_contents if is_broken else doc_node.flat_contents
189
+ return self.format_doc_ir(branch, indent_level, width_remaining, is_broken)
136
190
 
137
191
  elif isinstance(doc_node, doc.Align):
138
192
  align_spaces = doc_node.n if doc_node.n is not None else self.indent_size
139
- # effective_total_indent_spaces_for_children = (
140
- # indent_level * self.indent_size
141
- # ) + align_spaces
142
- child_indent_level_for_align = indent_level + (
143
- align_spaces // self.indent_size
144
- )
193
+ extra_levels = align_spaces // self.indent_size
194
+ child_indent_level = indent_level + extra_levels
145
195
 
146
- child_width_budget = width_remaining - align_spaces
147
- if child_width_budget < 0:
148
- child_width_budget = 0
196
+ # On the same line, alignment "consumes" part of the current budget.
197
+ child_width_budget = max(0, width_remaining - align_spaces)
149
198
 
150
199
  return self.format_doc_ir(
151
200
  doc_node.contents,
152
- child_indent_level_for_align, # Approximated level for Lines inside
153
- child_width_budget, # Budget for content on first line
201
+ child_indent_level,
202
+ child_width_budget,
154
203
  is_broken,
155
204
  )
205
+
156
206
  else:
157
207
  raise ValueError(f"Unknown DocType: {type(doc_node)}")
@@ -474,15 +474,15 @@ impl JacPlugin.visit_node
474
474
  glob expected_area = 78.53981633974483;
475
475
 
476
476
  test a1 {
477
- check assertAlmostEqual(calculate_area(RAD), expected_area);
477
+ assert assertAlmostEqual(calculate_area(RAD), expected_area);
478
478
  }
479
479
 
480
480
  test a2 {
481
481
  c = Circle(RAD);
482
- check assertAlmostEqual(c.area(), expected_area);
482
+ assert assertAlmostEqual(c.area(), expected_area);
483
483
  }
484
484
 
485
485
  test a3 {
486
486
  c = Circle(RAD);
487
- check assertEqual(c.shape_type, ShapeType.CIRCLE);
487
+ assert assertEqual(c.shape_type, ShapeType.CIRCLE);
488
488
  }
@@ -1,7 +1,6 @@
1
1
  with entry {
2
- triple_quoted_string =
3
- """This is a triple quoted string.
4
- It can span multiple lines.
5
- It can contain any number of quotes or apostrophes.
6
- """;
2
+ triple_quoted_string = """This is a triple quoted string.
3
+ It can span multiple lines.
4
+ It can contain any number of quotes or apostrophes.
5
+ """;
7
6
  }