Browse Source
An unquoted trailing -& element on a definition record line continues the
record on the next line. Implemented in
private::split_definition_records: the token is dropped and the next line
joins after a single space with its leading whitespace collapsed - exactly
how the Tcl parser joins backslash-newline continuations before a braced
definition ever reaches the splitter - so a -& record assembles
byte-identical to its backslash-continued equivalent. Motivation:
constructed (string-built) definitions cannot author backslash-newline
ergonomically (the building code's own quoting consumes it); -& is plain
text and survives any construction.
Collision rules (analysis and decision recorded in the goal detail file):
bare word preceded by whitespace (or whole line), trailing whitespace
tolerated; braced/quoted -& is data ({-&} escapes a literal trailing
value); mid-line -&, word-suffix abc-&, and -& inside still-open
braced/quoted values are data. Element-count disambiguation rejected
(action-at-a-distance; positional rule is locally decidable). Backslash
authoring unchanged - continuation is additive.
Tests: new recordcontinuation.test (6 tests) incl. backslash-twin
parse+render byte-equality and the constructed-def chaining case. Full
punk::args suite 181 pass / 1 pre-existing skip / 0 fail; full source-tree
suite 801 pass / 13 skip / 1 fail = exec-14.3 only (known baseline) - zero
regressions (tclsh 9.0.3). define doc documents -& alongside backslash
continuation.
Also recorded: tclparser considered and rejected for the splitter (ANSI
unbalanced-bracket data, dialect semantics outside Tcl grammar, binary
dependency vs G-004); dev-time parse cross-check lint flagged as candidate.
Assisted-by: harness=claude; primary-model=claude-fable-5; api-location=anthropic.com
master
6 changed files with 298 additions and 11 deletions
@ -1,4 +1,4 @@ |
|||||||
[project] |
[project] |
||||||
name = "punkshell" |
name = "punkshell" |
||||||
version = "0.12.5" |
version = "0.12.6" |
||||||
license = "BSD-2-Clause" |
license = "BSD-2-Clause" |
||||||
|
|||||||
@ -0,0 +1,182 @@ |
|||||||
|
package require tcltest |
||||||
|
|
||||||
|
package require punk::args |
||||||
|
package require punk::ansi |
||||||
|
|
||||||
|
#Record-continuation token -& (G-045): an unquoted trailing -& element continues a |
||||||
|
#definition record on the next line, rewritten internally to a backslash |
||||||
|
#line-continuation so the assembled record is byte-identical to its |
||||||
|
#backslash-continued equivalent. Key properties pinned here: |
||||||
|
# - equivalence: a -& definition parses AND renders identically to its |
||||||
|
# backslash-continuation twin (continuation is additive - backslash authoring |
||||||
|
# unchanged) |
||||||
|
# - the motivating case: constructed (string-built) definitions can use -& where |
||||||
|
# a backslash-newline would be consumed by the building code's own quoting |
||||||
|
# - collision/escape rules: a braced trailing {-&} is data; -& mid-line is data; |
||||||
|
# a word merely ending in the characters -& is data; -& on a line inside a |
||||||
|
# still-open braced value is data |
||||||
|
|
||||||
|
namespace eval ::testspace { |
||||||
|
namespace import ::tcltest::* |
||||||
|
variable common { |
||||||
|
set result "" |
||||||
|
} |
||||||
|
|
||||||
|
proc render_table {id} { |
||||||
|
return [punk::ansi::ansistrip [punk::args::arg_error "" [punk::args::get_spec $id] -aserror 0]] |
||||||
|
} |
||||||
|
|
||||||
|
#added 2026-07-12 (agent, G-045) |
||||||
|
#the two ids are deliberately the same length: the id appears in the rendered |
||||||
|
#synopsis, so differing lengths would change table geometry and defeat the |
||||||
|
#render-equality comparison after the id string map |
||||||
|
test recordcontinuation_amp_equivalent_to_backslash {a definition using trailing -& parses and renders identically to its backslash-continuation equivalent}\ |
||||||
|
-setup $common -body { |
||||||
|
punk::args::define { |
||||||
|
@id -id ::testspace::rc_bsl |
||||||
|
@cmd -name testspace::rc_fixture -summary\ |
||||||
|
"Fixture."\ |
||||||
|
-help\ |
||||||
|
"CFIRST line. |
||||||
|
CFLUSH line." |
||||||
|
@opts |
||||||
|
-o1 -type string -default od1 -help\ |
||||||
|
"opt help line1 |
||||||
|
opt help line2" |
||||||
|
@values -min 1 -max 1 |
||||||
|
v1 -type string -help\ |
||||||
|
"val help." |
||||||
|
} |
||||||
|
punk::args::define { |
||||||
|
@id -id ::testspace::rc_amp |
||||||
|
@cmd -name testspace::rc_fixture -summary -& |
||||||
|
"Fixture." -& |
||||||
|
-help -& |
||||||
|
"CFIRST line. |
||||||
|
CFLUSH line." |
||||||
|
@opts |
||||||
|
-o1 -type string -default od1 -help -& |
||||||
|
"opt help line1 |
||||||
|
opt help line2" |
||||||
|
@values -min 1 -max 1 |
||||||
|
v1 -type string -help -& |
||||||
|
"val help." |
||||||
|
} |
||||||
|
set r_b [punk::args::parse {-o1 X VAL1} withid ::testspace::rc_bsl] |
||||||
|
set r_a [punk::args::parse {-o1 X VAL1} withid ::testspace::rc_amp] |
||||||
|
lappend result [expr {[string map {rc_amp rc_bsl} $r_a] eq $r_b}] |
||||||
|
set t_b [render_table ::testspace::rc_bsl] |
||||||
|
set t_a [render_table ::testspace::rc_amp] |
||||||
|
lappend result [expr {[string map {rc_amp rc_bsl} $t_a] eq $t_b}] |
||||||
|
}\ |
||||||
|
-cleanup { |
||||||
|
punk::args::undefine ::testspace::rc_bsl 1 |
||||||
|
punk::args::undefine ::testspace::rc_amp 1 |
||||||
|
}\ |
||||||
|
-result [list 1 1] |
||||||
|
|
||||||
|
#added 2026-07-12 (agent, G-045) - the motivating case: string-built definitions |
||||||
|
#cannot use backslash-newline (consumed by the builder's own quoting) but can use -& |
||||||
|
test recordcontinuation_amp_constructed_definition {a constructed (string-built) definition using -& chains record lines, including into a multi-line quoted value}\ |
||||||
|
-setup $common -body { |
||||||
|
set def "" |
||||||
|
append def "@id -id ::testspace::rc_chain" \n |
||||||
|
append def "@cmd -name testspace::rc_chain -summary \"Chain.\" -help \"chain help\"" \n |
||||||
|
append def "@opts" \n |
||||||
|
append def "-o1 -type string -&" \n |
||||||
|
append def "-default HDEF -& " \n |
||||||
|
append def "-help \"h line1" \n |
||||||
|
append def "h line2\"" \n |
||||||
|
append def "@values -min 0 -max 0" |
||||||
|
punk::args::define $def |
||||||
|
set argd [punk::args::parse {} withid ::testspace::rc_chain] |
||||||
|
lappend result [dict get [dict get $argd opts] -o1] |
||||||
|
set t [render_table ::testspace::rc_chain] |
||||||
|
lappend result [expr {[string first "h line1" $t] >= 0}] |
||||||
|
lappend result [expr {[string first "h line2" $t] >= 0}] |
||||||
|
}\ |
||||||
|
-cleanup { |
||||||
|
punk::args::undefine ::testspace::rc_chain 1 |
||||||
|
}\ |
||||||
|
-result [list HDEF 1 1] |
||||||
|
|
||||||
|
#added 2026-07-12 (agent, G-045) |
||||||
|
test recordcontinuation_amp_braced_escape_is_data {a braced trailing {-&} is a literal value - the record ends and the next line is a separate record}\ |
||||||
|
-setup $common -body { |
||||||
|
punk::args::define { |
||||||
|
@id -id ::testspace::rc_braced |
||||||
|
@cmd -name testspace::rc_braced -summary "Braced." -help "braced help" |
||||||
|
@opts |
||||||
|
-o1 -type string -default {-&} |
||||||
|
-o2 -type string -default d2 |
||||||
|
@values -min 0 -max 0 |
||||||
|
} |
||||||
|
set argd [punk::args::parse {} withid ::testspace::rc_braced] |
||||||
|
lappend result [dict get [dict get $argd opts] -o1] |
||||||
|
lappend result [dict get [dict get $argd opts] -o2] |
||||||
|
}\ |
||||||
|
-cleanup { |
||||||
|
punk::args::undefine ::testspace::rc_braced 1 |
||||||
|
}\ |
||||||
|
-result [list -& d2] |
||||||
|
|
||||||
|
#added 2026-07-12 (agent, G-045) |
||||||
|
test recordcontinuation_amp_midline_is_data {an unquoted -& that is not the last element on the line is an ordinary value}\ |
||||||
|
-setup $common -body { |
||||||
|
punk::args::define { |
||||||
|
@id -id ::testspace::rc_midline |
||||||
|
@cmd -name testspace::rc_midline -summary "Midline." -help "midline help" |
||||||
|
@opts |
||||||
|
-o1 -default -& -type string |
||||||
|
@values -min 0 -max 0 |
||||||
|
} |
||||||
|
set argd [punk::args::parse {} withid ::testspace::rc_midline] |
||||||
|
lappend result [dict get [dict get $argd opts] -o1] |
||||||
|
}\ |
||||||
|
-cleanup { |
||||||
|
punk::args::undefine ::testspace::rc_midline 1 |
||||||
|
}\ |
||||||
|
-result [list -&] |
||||||
|
|
||||||
|
#added 2026-07-12 (agent, G-045) |
||||||
|
test recordcontinuation_amp_wordend_is_data {a word merely ending in the characters -& is not a continuation token}\ |
||||||
|
-setup $common -body { |
||||||
|
punk::args::define { |
||||||
|
@id -id ::testspace::rc_wordend |
||||||
|
@cmd -name testspace::rc_wordend -summary "Wordend." -help "wordend help" |
||||||
|
@opts |
||||||
|
-o1 -type string -default abc-& |
||||||
|
-o2 -type string -default d2 |
||||||
|
@values -min 0 -max 0 |
||||||
|
} |
||||||
|
set argd [punk::args::parse {} withid ::testspace::rc_wordend] |
||||||
|
lappend result [dict get [dict get $argd opts] -o1] |
||||||
|
lappend result [dict get [dict get $argd opts] -o2] |
||||||
|
}\ |
||||||
|
-cleanup { |
||||||
|
punk::args::undefine ::testspace::rc_wordend 1 |
||||||
|
}\ |
||||||
|
-result [list abc-& d2] |
||||||
|
|
||||||
|
#added 2026-07-12 (agent, G-045) |
||||||
|
test recordcontinuation_amp_inside_braces_is_data {a line ending in -& inside a still-open braced value stays literal data}\ |
||||||
|
-setup $common -body { |
||||||
|
punk::args::define { |
||||||
|
@id -id ::testspace::rc_inbrace |
||||||
|
@cmd -name testspace::rc_inbrace -summary "Inbrace." -help "inbrace help" |
||||||
|
@values -min 1 -max 1 |
||||||
|
v1 -type string -help {AFIRST -& |
||||||
|
ASECOND line} |
||||||
|
} |
||||||
|
set t [render_table ::testspace::rc_inbrace] |
||||||
|
lappend result [expr {[string first "AFIRST -&" $t] >= 0}] |
||||||
|
lappend result [expr {[string first "ASECOND line" $t] >= 0}] |
||||||
|
}\ |
||||||
|
-cleanup { |
||||||
|
punk::args::undefine ::testspace::rc_inbrace 1 |
||||||
|
}\ |
||||||
|
-result [list 1 1] |
||||||
|
|
||||||
|
cleanupTests |
||||||
|
} |
||||||
|
namespace delete ::testspace |
||||||
Loading…
Reference in new issue