Keep function body tail remarks in place - #330
Conversation
There was a problem hiding this comment.
Pull request overview
This PR fixes a long-standing formatter/serializer issue where own-line -- tail remarks inside FUNCTION/PROCEDURE/RULE bodies were being dropped, moved across IF/ELSE boundaries, or emitted in the wrong scope. It introduces a deterministic attachment strategy for “body comments” by attaching them to the following executable statement within the correct statement region and emitting them as leading remarks.
Changes:
- Add
RemarkInfo#placement(with"leading") and aModel::Statementmarker to distinguish executable statements and drive region-aware attachment/emission. - Update remark attachment/indexing so own-line body comments attach to the next statement in the same region (including ELSE awareness), with deterministic handling for terminal comments.
- Emit leading statement remarks above their owning statement and adjust ALIAS/REPEAT block-end remark emission to avoid losing or misplacing trailing remarks.
Reviewed changes
Copilot reviewed 21 out of 23 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| spec/syntax/remark_formatted.exp | Updates expected formatted EXPRESS output for remark indentation/placement in ALIAS/REPEAT bodies. |
| spec/syntax/mathematical_functions_schema/mathematical_functions_schema.yaml | Snapshot update reflecting recovered/moved untagged_remarks and new placement: leading serialization. |
| spec/fixtures/function_body_remarks.exp | Adds a fixture schema exercising leading body comments in IF/ELSE and REPEAT contexts. |
| spec/expressir/express/formatter_function_body_remarks_spec.rb | Adds coverage for leading body comment retention and key edge cases (ELSE, inline ELSE, repeat terminal comment). |
| lib/expressir/model/statements/*.rb | Marks executable statements (include Statement) and maps untagged_remarks for additional statement classes. |
| lib/expressir/model/remark_info.rb | Adds placement attribute and leading? helper; includes YAML/XML serialization for placement. |
| lib/expressir/model/concerns.rb | Introduces the Statement marker concern. |
| lib/expressir/model.rb | Autoloads the new Statement concern. |
| lib/expressir/express/remark_attacher.rb | Implements region-aware body-comment targeting (THEN vs ELSE), adds ELSE boundary detection, and records placement on attached remarks. |
| lib/expressir/express/node_position_index.rb | Tracks owner + collection for nodes to support region-aware remark attachment. |
| lib/expressir/express/formatters/statements_formatter.rb | Switches ALIAS/REPEAT to emit only appropriate block-end remarks with correct indentation. |
| lib/expressir/express/formatters/remark_formatter.rb | Adds leading-statement remark emission and block-end remark filtering logic. |
| lib/expressir/express/formatter.rb | Prepends leading remarks when formatting statement nodes. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 22 out of 24 changed files in this pull request and generated no new comments.
Suppressed comments (1)
lib/expressir/express/remark_attacher.rb:34
Setfrom Ruby stdlib is used in this class (e.g.,@attached_spans = Set.new,ancestors = Set.new) but the file doesn't require it. This makesRemarkAttacherfail to load in contexts wheresethasn't already been required (which also contradicts the nearby note about modules being loadable independently).
# Collections holding executable statements — the regions a leading
# body comment can belong to.
STATEMENT_REGIONS = %i[statements else_statements].freeze
# Matches the ELSE keyword opening a line or following a statement
# terminator, optionally trailed by an inline tail remark.
ELSE_BOUNDARY = /\A(?:.*;)?\s*ELSE(?:\s*--.*)?\z/i
…ormance Cache remark ownership lookup
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 23 out of 25 changed files in this pull request and generated no new comments.
Suppressed comments (1)
lib/expressir/model/model_element.rb:308
ModelElement#to_snow forcesno_remarks: true, which changes the implicit output of string interpolation ("#{element}") to always drop remarks. Ifto_sis part of the public surface (used by callers to render full EXPRESS including remarks), this is a breaking behavioral change that’s not obviously related to this PR’s remark-placement fix.
Consider keeping to_s as the full formatter (default no_remarks: false) and letting callers explicitly request remark-free output via format(no_remarks: true).
# No-arg delegator so string interpolation ("#{element}") still
# produces EXPRESS source rather than the default Object#to_s.
# set default formatter to no_remarks: true
def to_s
format(no_remarks: true)
end
|
@HassanAkbar is this ready to be merged? We need it released. Thanks! |
|
@ronaldtse While testing I found a few edge cases, they are fixed in this PR now:
Once the Copilot review is green, this PR is ready to merge on my end. These are not implemented here, I'll create separate PRs for them:
All three behave the same as |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 34 out of 36 changed files in this pull request and generated no new comments.
Suppressed comments (2)
spec/expressir/express/remark_attacher_performance_spec.rb:68
- This example uses wall-clock timing (Benchmark.realtime) to detect quadratic behavior, which can be flaky across CI runners and load. The previous example already asserts the key invariant (build_active_scope_map is built once); consider replacing this timing assertion with another deterministic call-count assertion on the larger input, and drop the Benchmark dependency.
small_time = Benchmark.realtime { Expressir::Express::Parser.from_exp(source) }
big_time = Benchmark.realtime { Expressir::Express::Parser.from_exp(big) }
expect(big_time).to be < (small_time * 12)
spec/expressir/express/remark_attacher_performance_spec.rb:2
- After removing the Benchmark-based timing assertion, this require becomes unused. It can be dropped to keep the spec lean and avoid loading extra stdlib code.
This issue also appears on line 65 of the same file.
require "benchmark"
Tail comments inside FUNCTION, PROCEDURE, and RULE bodies get lost or misplaced during formatting. This fixes their attachment and their emission.
The bug
For a
--comment written on its own line inside a body:END_REPEAT.END_FUNCTIONas a trailer.On one large production STEP schema, only 19 of 64 body comments survived formatting.
Example, before this fix:
formatted with the STEP-1 block missing. It now formats with the comments exactly where they were written.
The fix
RemarkInfo#placementfield marks these as"leading".nilplacement keeps exact legacy behavior. Old caches load unchanged.Evidence
no_remarksoutput is unchanged.formatter_function_body_remarks_spec.rb) covers all four failure patterns plus ELSE, terminal, and schema-level edge cases.Notes for reviewers
mathematical_functions_schemaYAML snapshot changed. Zero remark texts were lost; 108 were recovered. Those 108 previously fell off statement nodes during serialization because statements never mappeduntagged_remarks.untagged_remarks; a newModel::Statementmarker identifies executable statements.Ifnow declareselse_statementsas a traversed collection.ELSEorEND_*with no following statement keeps the old behavior, and CASE bodies are untouched.