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