Skip to content

Commit ed326a0

Browse files
committed
M7 Phase 1: thread %field_vals as a per-call lexical
Turn do_form's `local %field_vals` into an explicit per-call lexical `my $field_vals = {}` and pass it into the nested load_persistent() as a parameter, replacing the dynamic-scope dependency. Six sites, all inside do_form. Design note carried forward to Phases 2-3: %field_vals is per-call recursion state, so it becomes a per-call lexical, NOT a key on the single startup $ctx -- a shared $ctx->{field_vals} reset per call would clobber a parent screen when forms nest, whereas the lexical preserves the old `local` save/restore exactly. The startup $ctx stays for genuinely shared state (cfg/process); per-screen state lives in per-call containers. load_persistent taking the map explicitly sidesteps the named-sub closure trap (a named inner sub binds the first call's lexical, not each call's). Inventory correction: only load_persistent read %field_vals, not sync_fields_val/save_persistent as the plan assumed. Coverage gap closed: the gate the plan named did not exist -- the pty tests only navigate default/empty fields. Added t/19-form-init.t, which drives a form whose `init { command:... }` pre-fills a field and asserts the value renders, exercising the field_vals write/read sites this phase changed. Full suite now 309 tests; all four CI checks pass locally. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent c30e54e commit ed326a0

4 files changed

Lines changed: 96 additions & 17 deletions

File tree

M7-CTX-PLAN.md

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -78,13 +78,25 @@ threading mistake shows up as a crash or a mis-drawn screen, not a silent pass.
7878
need `our`-declaring in place; moving ~120 of them up front is churn/risk for
7979
no near-term gain. Handle at Phase 6 (or skip — it is optional polish).
8080

81-
### Phase 1 — `%field_vals``$ctx->{field_vals}` *(8 sites — the pattern-setter)*
82-
- Smallest structure, but it is read by the **nested** `sync_fields_val` /
83-
`save_persistent` / `load_persistent`, so this phase *is* where the §2
84-
nested-sub conversion gets proven on a small surface.
85-
- Convert those three inner subs to take `$ctx` (option b). Replace `local
86-
%field_vals` with `$ctx->{field_vals} = {}` per `do_form` call.
87-
- **Gate:** `t/08` (field defaults/values) + persistent-save tests must pass.
81+
### Phase 1 — `%field_vals` → per-call lexical *(done — the pattern-setter)*
82+
- Reality check vs. the plan: only **one** nested sub actually reads
83+
`%field_vals``load_persistent` (not `sync_fields_val`/`save_persistent`,
84+
which the inventory had wrongly assumed). 6 sites total, all inside `do_form`.
85+
- **Design refinement (applies to Phases 2–3 too):** `%field_vals` is `local`,
86+
i.e. *per-call* recursion state, so it became a per-call **lexical**
87+
`my $field_vals = {}`**not** a key on the single startup `$ctx`. A shared
88+
`$ctx->{field_vals} = {}` per call would clobber a parent screen on nesting;
89+
the lexical preserves the old `local` save/restore exactly. The startup `$ctx`
90+
stays for the genuinely shared state (cfg/process). So per-screen state
91+
(field_vals, menu, form, fp, cform) lives in per-call containers; `$ctx` holds
92+
the shared parts. Nested `load_persistent` takes the map as an explicit
93+
parameter (option b), sidestepping the named-sub closure trap.
94+
- **Coverage gap closed:** the gate the plan named ("field-values + persistent
95+
tests") did not exist — the pty tests only navigate default/empty fields.
96+
Added `t/19-form-init.t`, which drives a form whose `init { command:… }`
97+
pre-fills a field and asserts the value renders, exercising the field_vals
98+
write (init parse) and read (field creation) sites this phase changed.
99+
- **Gate:** full suite (309 tests) + the new `t/19` green.
88100

89101
### Phase 2 — `%menu``$ctx->{menu}` *(38 sites)*
90102
- `load_menu` fills `$ctx->{menu}` (it already returns a structure from

ROADMAP.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -205,9 +205,12 @@ the conformance tests.
205205
- 🔄 **`$ctx` threading** (in progress): replacing `ccfe.pl`'s package globals
206206
and `local` dynamic scope with one explicit run-state object. Planned in
207207
**M7-CTX-PLAN.md** (phases 0–6, all in scope). **Phase 0 done:**
208-
`CCFE::Context::new()` container built at startup (unused until Phase 1),
209-
`t/18-context.t`, and the latent `perl (>= 5.36.0)` floor added to
210-
`debian/control`. Next: Phase 1 (`%field_vals`, the nested-sub pattern-setter).
208+
`CCFE::Context::new()` container built at startup, `t/18-context.t`, and the
209+
latent `perl (>= 5.36.0)` floor added to `debian/control`. **Phase 1 done:**
210+
`%field_vals` turned from a `local` global into a per-call lexical threaded
211+
explicitly into `load_persistent` (proving the named-sub conversion on the
212+
smallest surface); `t/19-form-init.t` added to cover the previously-untested
213+
init→field-value path. 309 tests green. Next: Phase 2 (`%menu`).
211214

212215
## M8 — Non-functional close-out audit _(final gate)_
213216
Five dimensions: **test coverage, code quality, performance, security,

src/ccfe.pl

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2651,7 +2651,11 @@ sub do_form {
26512651
$hidden, $hscroll, $script, $val, $form_dir
26522652
);
26532653
my ($fpad);
2654-
local %field_vals;
2654+
# M7 de-globalisation (Phase 1): the per-form field-value map is a per-call
2655+
# lexical, not a shared global -- this preserves the old `local %field_vals`
2656+
# recursion semantics (a nested screen gets its own) and is threaded into
2657+
# the nested load_persistent() explicitly rather than via dynamic scope.
2658+
my $field_vals = {};
26552659
local %form;
26562660
my ( @fields_to_remove, @fields_to_enable, @fields_to_disable );
26572661

@@ -2855,6 +2859,7 @@ sub do_form {
28552859
}
28562860

28572861
sub load_persistent {
2862+
my ($field_vals) = @_;
28582863
my @res = ();
28592864
my ( $fname, $hash );
28602865

@@ -2876,7 +2881,7 @@ sub do_form {
28762881
next if /^\s*#/;
28772882
chop;
28782883
my ( $id, $val ) = split /\s*=\s*/;
2879-
$field_vals{$id} = $val;
2884+
$field_vals->{$id} = $val;
28802885
trace( " $id=\"$val\"", $LOG_FIELDS_VAL );
28812886
}
28822887
}
@@ -2916,7 +2921,7 @@ sub do_form {
29162921
if ( $action eq 'command' ) {
29172922
curs_set($OFF) if $HIDE_CURSOR;
29182923
my ( $wpan, $wwin ) = open_wait_msg;
2919-
undef %field_vals;
2924+
%{$field_vals} = ();
29202925
my $prev_path = $ENV{PATH};
29212926

29222927
my @res = ();
@@ -2963,7 +2968,7 @@ sub do_form {
29632968
; #$LOG_FIELDS_VAL
29642969
}
29652970
else {
2966-
$field_vals{$id} = $val;
2971+
$field_vals->{$id} = $val;
29672972
trace( " $s=\"$val\"", $LOG_FIELDS_VAL );
29682973
}
29692974
}
@@ -2979,7 +2984,7 @@ sub do_form {
29792984
trace("unknown form init type \"$action\"");
29802985
}
29812986
}
2982-
load_persistent;
2987+
load_persistent($field_vals);
29832988

29842989
$y = 0;
29852990
$npages = 0;
@@ -3012,7 +3017,8 @@ sub do_form {
30123017
$all_ids .= " " if !( $form{fields}[$i]{option} );
30133018
$all_ids .= "%{$id}" if $id !~ /^$FSEP_ID_PRFX/;
30143019
if ( $type == $SEPARATOR and !defined($label) ) {
3015-
$label = $field_vals{$id} ? $field_vals{$id} : 'ERROR!';
3020+
$label =
3021+
$field_vals->{$id} ? $field_vals->{$id} : 'ERROR!';
30163022
}
30173023
if ( $LAYOUT == $SIMPLE ) {
30183024
$len = $COLS - ( 52 + $rflags_size + 2 )
@@ -3026,7 +3032,7 @@ sub do_form {
30263032
my $lw = disp_width($label); # label width in columns
30273033
$val = '';
30283034
$val = $default if defined($default);
3029-
$val = $field_vals{$id} if defined( $field_vals{$id} );
3035+
$val = $field_vals->{$id} if defined( $field_vals->{$id} );
30303036
$val = substr( $val, 0, $len )
30313037
if ( $hscroll == $NO )
30323038
and ( length($val) > $len );

src/t/19-form-init.t

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
#!/usr/bin/perl
2+
#
3+
# Form init command -> field value population (regression for M7 Phase 1).
4+
#
5+
# A form's `init { command:... }` block runs a command whose `id=value` stdout
6+
# lines pre-fill the form's fields. Those values flow through do_form's
7+
# per-form field-value map -- the `%field_vals` that M7 Phase 1 turned from a
8+
# `local` global into an explicit per-call lexical threaded into
9+
# load_persistent(). The existing pty tests only navigate a form of default
10+
# (empty) fields, so this drives the populated path end to end: it asserts the
11+
# init command's value actually renders in the field, which exercises the
12+
# field_vals write (init parse) and read (field creation) sites Phase 1 changed.
13+
#
14+
use strict;
15+
use warnings;
16+
use FindBin qw($Bin);
17+
use lib "$Bin/lib";
18+
use File::Temp qw(tempdir);
19+
use Test::More;
20+
21+
my $src = "$Bin/..";
22+
23+
eval { require CCFE::Test::Pty; 1 } or plan skip_all => "pty helper: $@";
24+
plan skip_all => 'no Linux pseudo-terminal' unless CCFE::Test::Pty->available;
25+
plan skip_all => 'Curses not installed' unless eval { require Curses; 1 };
26+
plan skip_all => 'no installer' unless -f "$src/install.sh";
27+
28+
my $prefix = tempdir( CLEANUP => 1 );
29+
my $log = `cd "$src" && sh install.sh -b -p "$prefix" 2>&1`;
30+
plan skip_all => "install failed: $log" unless $? == 0 && -x "$prefix/bin/ccfe";
31+
32+
# A one-field form whose init command pre-fills that field with a known marker.
33+
my $objs = "$prefix/share/ccfe/objects/ccfe";
34+
open( my $fh, '>', "$objs/initval.form" ) or plan skip_all => "write: $!";
35+
print {$fh} <<'FORM';
36+
title { Init value test }
37+
field {
38+
id = GREETING
39+
len = 20
40+
type = STRING
41+
label = Greeting
42+
}
43+
init { command:printf 'GREETING=hello-init\n' }
44+
action { run:true }
45+
FORM
46+
close($fh);
47+
48+
plan tests => 2;
49+
50+
my $pty = CCFE::Test::Pty->spawn( 80, 24, "$prefix/bin/ccfe", 'initval' );
51+
$pty->pump(1.3);
52+
my $screen = $pty->screen;
53+
54+
like( $screen, qr/Greeting/, 'the form opens and shows the field label' );
55+
like( $screen, qr/hello-init/,
56+
'the init command value is populated into the field (field_vals path)' );
57+
58+
$pty->send("\e"); # leave the form; DESTROY reaps the child

0 commit comments

Comments
 (0)