diff --git a/jac/jaclang/compiler/absyntree.py b/jac/jaclang/compiler/absyntree.py index c3873ba52..4f1ba5352 100644 --- a/jac/jaclang/compiler/absyntree.py +++ b/jac/jaclang/compiler/absyntree.py @@ -2873,13 +2873,24 @@ def normalize(self, deep: bool = False) -> bool: if deep: res = self.parts.normalize(deep) if self.parts else res new_kid: list[AstNode] = [] + is_single_quote = ( + isinstance(self.kid[0], Token) and self.kid[0].name == Tok.FSTR_SQ_START + ) if self.parts: + if is_single_quote: + new_kid.append(self.gen_token(Tok.FSTR_SQ_START)) + else: + new_kid.append(self.gen_token(Tok.FSTR_START)) for i in self.parts.items: if isinstance(i, String): i.value = ( "{{" if i.value == "{" else "}}" if i.value == "}" else i.value ) new_kid.append(self.parts) + if is_single_quote: + new_kid.append(self.gen_token(Tok.FSTR_SQ_END)) + else: + new_kid.append(self.gen_token(Tok.FSTR_END)) self.set_kids(nodes=new_kid) return res @@ -4257,7 +4268,6 @@ def lit_value(self) -> str: """Return literal value in its python type.""" if isinstance(self.value, bytes): return self.value - prefix_len = 3 if self.value.startswith(("'''", '"""')) else 1 if any( self.value.startswith(prefix) and self.value[len(prefix) :].startswith(("'", '"')) @@ -4266,8 +4276,23 @@ def lit_value(self) -> str: return eval(self.value) elif self.value.startswith(("'", '"')): - ret_str = self.value[prefix_len:-prefix_len] - return ret_str.encode().decode("unicode_escape", errors="backslashreplace") + repr_str = self.value.encode().decode("unicode_escape") + if ( + self.value.startswith('"""') + and self.value.endswith('"""') + and not self.find_parent_of_type(FString) + ): + return repr_str[3:-3] + if (not self.find_parent_of_type(FString)) or ( + not ( + self.parent + and isinstance(self.parent, SubNodeList) + and self.parent.parent + and isinstance(self.parent.parent, FString) + ) + ): + return repr_str[1:-1] + return repr_str else: return self.value diff --git a/jac/jaclang/compiler/passes/main/pyast_load_pass.py b/jac/jaclang/compiler/passes/main/pyast_load_pass.py index 863e4cf5e..64d3b6deb 100644 --- a/jac/jaclang/compiler/passes/main/pyast_load_pass.py +++ b/jac/jaclang/compiler/passes/main/pyast_load_pass.py @@ -1148,14 +1148,16 @@ class Constant(expr): else: token_type = f"{value_type.__name__.upper()}" + if value_type == str: + raw_repr = repr(node.value) + quote = "'" if raw_repr.startswith("'") else '"' + value = f"{quote}{raw_repr[1:-1]}{quote}" + else: + value = str(node.value) return type_mapping[value_type]( file_path=self.mod_path, name=token_type, - value=( - f'"{repr(node.value)[1:-1]}"' - if value_type == str - else str(node.value) - ), + value=value, line=node.lineno, end_line=node.end_lineno if node.end_lineno else node.lineno, col_start=node.col_offset, @@ -2625,7 +2627,7 @@ def proc_type_var_tuple(self, node: py_ast.TypeVarTuple) -> None: def convert_to_doc(self, string: ast.String) -> None: """Convert a string to a docstring.""" - string.value = f'""{string.value}""' + string.value = f'"""{string.value[1:-1]}"""' def aug_op_map(self, tok_dict: dict, op: ast.Token) -> str: """aug_mapper.""" diff --git a/jac/jaclang/compiler/passes/main/tests/fixtures/fstrings.jac b/jac/jaclang/compiler/passes/main/tests/fixtures/fstrings.jac index 5a2705aa8..be3ad2cbd 100644 --- a/jac/jaclang/compiler/passes/main/tests/fixtures/fstrings.jac +++ b/jac/jaclang/compiler/passes/main/tests/fixtures/fstrings.jac @@ -1,4 +1,47 @@ """Small test for fstrings.""" +glob apple = 2; +glob a=f"hello {40 + 2} wor{apple}ld "; -glob a=f"hello {40 + 2} wor{apple}ld "; \ No newline at end of file +with entry { + a = 9; + s_2 = "'''hello'''"; + s_3 = "'''hello '''"; + s_4 = "''' hello'''"; + s_5 = '""" hello"""'; + s_6 = '"""hello"""'; + s_7 = '"""hello"" "'; + s_8 = '"""hello""" '; + print( + len(s_2), + len(s_3), + len(s_4), + len(s_5), + len(s_6), + len(s_7), + len(s_8) + ) ; + b1 = f"{"'''hello''' "}"; + b_2 = f"{'"""hello""" '}"; + f_1 = f"{'hello '}{a}{'"""hello"""'}"; + f_2 = f"{'hello '}{a}{"'''hello'''"}"; + print(len(b1),len(b_2), b_2,len(f_1), len(f_2)) ; + f_3 = f"{'"""again"""'}"; + f_4 = f"{'"""again""" '}"; + f_5 = f"{"'''again'''"}"; + f_6 = f"{"'''again''' "}"; + f_7 = f"{'"""again"""'}"; + f_s1 = f"{'hello '}{a}{'"""hello"""'}"; + f_s2 = f"{'hello '}{a}{"'''hello'''"}{'kklkl'}"; + print( + len(f_3), + len(f_4), + len(f_5), + len(f_6), + len(f_7), + len(f_s1), len(f_s2) + ) ; + """sdfsdf\nsdfsdfsdfsd dffgdfgd.""" ; +} +can func() {; +} diff --git a/jac/jaclang/tests/test_language.py b/jac/jaclang/tests/test_language.py index 5bcb1fab6..20d926826 100644 --- a/jac/jaclang/tests/test_language.py +++ b/jac/jaclang/tests/test_language.py @@ -101,7 +101,7 @@ def test_chandra_bugs(self) -> None: stdout_value = captured_output.getvalue() self.assertEqual( stdout_value, - "\nTrue\n", ) def test_chandra_bugs2(self) -> None: @@ -212,6 +212,20 @@ def test_raw_bytestr(self) -> None: self.assertEqual(stdout_value.count(r"\\\\"), 2) self.assertEqual(stdout_value.count(""), 3) + def test_fstring_multiple_quotation(self) -> None: + """Test fstring with multiple quotation.""" + captured_output = io.StringIO() + sys.stdout = captured_output + jac_import( + "compiler/passes/main/tests/fixtures/fstrings", + base_path=self.fixture_abs_path("../../"), + ) + sys.stdout = sys.__stdout__ + stdout_value = captured_output.getvalue() + self.assertEqual(stdout_value.split("\n")[0], "11 13 12 12 11 12 12") + self.assertEqual(stdout_value.split("\n")[1], '12 12 """hello""" 18 18') + self.assertEqual(stdout_value.split("\n")[2], "11 12 11 12 11 18 23") + def test_deep_imports(self) -> None: """Parse micro jac file.""" captured_output = io.StringIO() @@ -511,13 +525,11 @@ def test_pyfunc_1(self) -> None: self.assertIn("can greet2(**kwargs: Any)", output) self.assertEqual(output.count("with entry {"), 13) self.assertIn( - '"""Enum for shape types"""\nenum ShapeType{ CIRCLE = "Circle",\n', + '"""Enum for shape types"""\nenum ShapeType{ CIRCLE = \'Circle\',\n', output, ) - self.assertIn( - "UNKNOWN = \"Unknown\",\n::py::\nprint('hello')\n::py::\n }", output - ) - self.assertIn('assert x == 5 , "x should be equal to 5" ;', output) + self.assertIn("\nUNKNOWN = 'Unknown',\n::py::\nprint('hello')\n::", output) + self.assertIn("assert x == 5 , 'x should be equal to 5' ;", output) self.assertIn("if not x == y {", output) self.assertIn("can greet2(**kwargs: Any) {", output) self.assertIn("squares_dict = {x: (x ** 2) for x in numbers};", output) @@ -783,7 +795,7 @@ def test_deep_convert(self) -> None: settings.print_py_raised_ast = True ir = jac_pass_to_pass(py_ast_build_pass, schedule=py_code_gen_typed).ir jac_ast = ir.pp() - self.assertIn(' | +-- String - "Loop compl', jac_ast) + self.assertIn(" | +-- String - 'Loop completed normally{}'", jac_ast) self.assertEqual(len(ir.get_all_sub_nodes(ast.SubNodeList)), 269) captured_output = io.StringIO() sys.stdout = captured_output