feat(repl): print the session model as notation at the prompt - #273
Conversation
%print writes the whole session model back as SysML v2 notation, and %print <name> one element and its body, through the writer %save writes .sysml with: export.SysMLElement formats one element's source via the same format.Source path, so comments and text as typed survive and a print can be submitted again. Printing is a read: no runtime object, no change to instances, the buffer, or an active %action/%state debugging session. Co-Authored-By: jason.han <jason.han@jpl.nasa.gov>
Original prompt from Devin Bot
|
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
E2E test of
|
| func declarationSpan(sym *symbols.Symbol) source.Span { | ||
| span := sym.DeclSpan | ||
| if sym.Decl != nil { | ||
| span = sym.Decl.Span() | ||
| } | ||
| start := span.Offset | ||
| for _, tr := range sym.LeadingTrivia { | ||
| if tr.Kind == ast.TriviaWhitespace || tr.Span.Offset >= span.Offset { | ||
| continue | ||
| } | ||
| if tr.Span.Offset < start { | ||
| start = tr.Span.Offset | ||
| } | ||
| } | ||
| return source.Span{Offset: start, Len: span.End() - start} | ||
| } |
There was a problem hiding this comment.
🟡 Printing one element also prints the note written for the element that follows it
The printed slice of the model is cut at the start of the next declaration (span.End() from sym.Decl.Span() at internal/repl/print.go:79) rather than at the end of the element itself, so any comment lines sitting between the two are printed as part of the first element.
Impact: A user printing one element sees a comment that belongs to the next element appended below it, and the same comment appears again when that next element is printed.
Why the declaration span runs past the element into the following trivia
The parser builds a declaration's span with spanFrom(start), whose end is the current token's offset (internal/core/parser/parser.go:270-276). Token offsets skip trivia (internal/core/parser/parser.go:97-114 records whitespace/comments as pending trivia and never widens a token's span), so a declaration's span ends where the next real token begins — after every blank line and comment written between them.
declarationSpan (internal/repl/print.go:76-91) takes that end verbatim and only extends the start backwards over leading comment trivia, so for
part def Engine { … }
// how heavy it is
part def Vehicle { … }
%print Engine yields the source part def Engine { … }\n\n// how heavy it is\n; strings.TrimSpace in export.SysMLElement (internal/core/export/convert.go:211) removes only the whitespace, leaving the note attached to Engine. %print Vehicle prints the same note again, since LeadingTrivia pulls the start back to it. The existing tests miss this because internal/repl/testdata/vehicle_package.sysml has no comments between members, and TestPrintKeepsComments prints the last member of its package (whose span ends at the closing brace).
A fix would clamp the span's end to the end of the element's last real token (e.g. trim trailing trivia, or record the declaration's end at the last consumed token rather than at the next token's start).
Prompt for agents
`declarationSpan` in internal/repl/print.go uses `sym.Decl.Span()` as the extent of the element to print. Parser spans are built with `spanFrom(start)`, whose end is the offset of the *next* token, and token offsets exclude trivia — so the span reaches past the element's final `;`/`}` over the blank lines and comments that follow, which are exactly the notes belonging to the next declaration. As a result `%print <name>` appends the following element's comment to the printed element, and printing that next element shows the comment a second time (its own leading-trivia walk pulls the start back to it). The fix should bound the printed span at the end of the element's own last non-trivia token: either trim trailing trivia/comment text off the span in `declarationSpan` (the symbol has no trailing-trivia record, so trimming may need to scan the buffer text back to the last non-whitespace, non-comment byte), or make the parser record a declaration's end at the last consumed token (`lastEnd()`), which would need care since many span-based behaviors depend on `spanFrom`.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
Confirmed and fixed in 0a44bc5. Reproduced against the built REPL first — %print Demo::Engine printed part def Engine { … } followed by the // how heavy it is note belonging to Vehicle, and %print Demo::Vehicle printed it again.
Fixed in export.SysMLElement rather than in the parser, since spanFrom's end-at-next-token semantics is depended on widely: the element's source is now cut at its last token that is neither whitespace nor a comment/note, lexing the slice for that end.
func trimTrailingTrivia(text string) string {
lx := lexer.New(source.New("element", []byte(text)))
end := 0
for tok := lx.Next(); tok.Kind != lexer.EOF; tok = lx.Next() {
if tok.IsTrivia() || tok.Kind == lexer.RegularComment {
continue
}
end = tok.Span.End()
}
return strings.TrimSpace(text[:end])
}Leading notes are still included (only the end moves), and a span holding nothing but a comment now reports ErrNoNotation instead of writing an empty document. Regression tests: repl/print_test.go:TestPrintStopsBeforeTheNextElementsComment (the note prints with Car and not with Engine) and export/export_test.go:TestSysMLElementDropsTrailingComments. Full gate re-run clean, corpus still 98/100.
A declaration's span ends where the next token begins, so it runs over the blank lines and comments between the two. export.SysMLElement now cuts the source at the element's last real token, so the note belonging to what follows is printed with that element only. Co-Authored-By: jason.han <jason.han@jpl.nasa.gov>
Summary
Seeing the model a session holds required
%save <file>and another program to open the file.%printwrites it back at the prompt instead, and%print <name>writes one element and its body, taking the quoted/qualified spellings the other name commands take (%print 'My Pkg'::Car,%print Top::'My Pkg'::Car).It is not a second renderer: the whole-buffer print is
export.ConvertTolerant(sysml → sysml), the same call%save's.sysmlpath makes, and the one-element print goes through a newexport.SysMLElementthat runs one element's source through that sameformat.Source:Because the printed text is the source (re-indented), comments survive and a print submitted again rebuilds the same model —
TestPrintRoundTripsThroughSubmitsubmits a print into a fresh session and asserts the reprint is byte-identical.The element's span is its declaration plus the non-whitespace leading trivia above it, so the note explaining a definition prints with it (
declarationSpan).Printing is a read, and tested as one:
printSessionusess.text()(nevergetOrCreateRuntime), so no object is materialized and%instances,%list, the buffer and a running%action/%statedebugging session are unchanged across a print — including a print of an unresolvable name. Notation only, so no RDF notice follows a print. Three one-line answers replace silence: an empty session, a name this session declares nowhere (a library symbol carrying no source), and a declaration spanning no source. A buffer with syntax errors prints as typed with awarning:prefix, matching how%savereports it.Tab completion needed no change —
%printis not a path command, so it falls through to the generic name-completion branch ofcomplete.go;TestPrintCompletionpins that both the command and names after it complete.Verification
OMG corpus gate, run locally: 98/100 training files clean (the known baseline; 2 files / 4 errors are pinned OMG source bugs).
internal/core/model/testdata/training_examples_expected.txtis untouched.Link to Devin session: https://nasa-jpl-demo.devinenterprise.com/sessions/d15785b463e7487eaa58dc9513d22478
Requested by: @HuiJun