Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- **Formatter: Bare GenAI Ability `by` and Semicolon Preservation**: The Jac formatter no longer strips the `by` keyword and trailing semicolon from bare genai ability declarations (e.g. `def classify(s: str) -> str by llm;`). Previously, formatting such a declaration would silently drop `by <model>;`, producing invalid output. The fix inspects child tokens of the `Name` node in `DocIRGenPass.exit_name` and correctly renders the full `by <name>;` fragment when present. Formatting is also idempotent.
33 changes: 30 additions & 3 deletions jac/jaclang/compiler/passes/tool/impl/doc_ir_gen_pass.impl.jac
Original file line number Diff line number Diff line change
Expand Up @@ -284,10 +284,37 @@ impl DocIRGenPass.exit_int(nd: uni.Int) -> None {

"""Generate DocIR for names."""
impl DocIRGenPass.exit_name(nd: uni.Name) -> None {
if nd.is_kwesc {
nd.gen.doc_ir = self.text(f"`{nd.value}", source_token=nd);
name_text = f"`{nd.value}" if nd.is_kwesc else nd.value;
by_kid = next(
(
k
for k in nd.kid
if isinstance(k, uni.Token) and k.name == Tok.KW_BY
),
None
);
if by_kid is not None {
# Bare genai body (`by <name>;`): the parser attaches `by` and `;`
# as kids of this Name node. Render them so they are not lost.
semi_kid = next(
(
k
for k in nd.kid
if isinstance(k, uni.Semi)
),
None
);
parts: list[doc.DocType] = [
by_kid.gen.doc_ir,
self.space(),
self.text(name_text, source_token=nd)
];
if semi_kid is not None {
parts.append(semi_kid.gen.doc_ir);
}
nd.gen.doc_ir = self.concat(parts, ast_node=nd);
} else {
nd.gen.doc_ir = self.text(nd.value, source_token=nd);
nd.gen.doc_ir = self.text(name_text, source_token=nd);
}
}

Expand Down
24 changes: 22 additions & 2 deletions jac/tests/compiler/passes/tool/test_jac_format_pass.jac
Original file line number Diff line number Diff line change
Expand Up @@ -518,6 +518,27 @@ test "jsx attributes and children group independently" {
);
}

"""Regression: formatter must preserve `by <name>;` (bare genai ability
without parentheses) — previously both `by` and `;` were stripped."""
test "genai ability bare name preserves by and semicolon" {
source = "def classify(s: str) -> str by llm;\n";
Comment thread
Developer-Linus marked this conversation as resolved.
prog = JacProgram.jac_str_formatter(source_str=source, file_path="<test>.jac");
assert not prog.errors_had , f"formatter errors: {prog.errors_had}";
formatted = prog.mod.main.gen.jac;
assert "by" in formatted , (
f"`by` keyword was stripped from genai ability:\n{repr(formatted)}"
);
assert formatted.rstrip().endswith(";") , (
f"semicolon was stripped from genai ability:\n{repr(formatted)}"
);
assert "by llm;" in formatted , (
f"expected `by llm;` in formatted output, got:\n{repr(formatted)}"
);
# Idempotency
prog2 = JacProgram.jac_str_formatter(source_str=formatted, file_path="<test>.jac");
assert formatted == prog2.mod.main.gen.jac , "genai bare-name formatting is not idempotent";
}

"""Regression: jac_str_formatter must populate errors_had on syntax error
so that the LSP returns the original source instead of an empty string."""
test "syntax error preserves original source" {
Expand All @@ -533,7 +554,6 @@ test "syntax error preserves original source" {
# return formatted if no errors, else return original source
result = gen_output if not prog.errors_had else bad_source;
assert result == bad_source , (
"Formatter must return original source on syntax error, "
f"got: {repr(result)}"
"Formatter must return original source on syntax error, " f"got: {repr(result)}"
);
}
Loading