File Coverage

blib/lib/Syntax/Feature/With.pm
Criterion Covered Total %
statement 115 115 100.0
branch 64 70 91.4
condition 26 30 86.6
subroutine 11 11 100.0
pod 2 2 100.0
total 218 228 95.6


line stmt bran cond sub pod time code
1             package Syntax::Feature::With;
2              
3 13     13   2062456 use strict;
  13         32  
  13         603  
4 13     13   74 use warnings;
  13         27  
  13         1036  
5              
6 13     13   89 use Carp 'croak';
  13         26  
  13         895  
7 13     13   95 use Exporter 'import';
  13         58  
  13         687  
8 13     13   6627 use PadWalker qw(closed_over set_closed_over);
  13         10529  
  13         23483  
9              
10             our @EXPORT_OK = qw(with with_hash);
11             our @EXPORT = qw(with with_hash);
12              
13             our $VERSION = '0.02';
14              
15             # Track nested with() depth for trace/debug output
16             my $WITH_DEPTH = 0;
17              
18             =head1 NAME
19              
20             Syntax::Feature::With - Simulate Pascal's "with" statement in Perl
21              
22             =head1 VERSION
23              
24             Version 0.02
25              
26             =head1 SYNOPSIS
27              
28             use Syntax::Feature::With qw(with with_hash);
29              
30             my %h = ( a => 1, b => 2 );
31             my ($a, $b);
32              
33             # Basic usage
34             with \%h, sub {
35             say $a; # 1
36             $b = 99; # updates %h
37             };
38              
39             # Strict mode
40             with -strict => \%h, sub {
41             say $a; # ok
42             say $b; # ok
43             say $c; # error: undeclared
44             };
45              
46             # Debug mode
47             with -debug => \%h, sub {
48             ...
49             };
50              
51             # Trace mode (includes debug)
52             with -trace => \%h, sub {
53             ...
54             };
55              
56             # Convenience wrapper
57             with_hash %h => sub {
58             say $a;
59             };
60              
61             =head1 DESCRIPTION
62              
63             C provides a simple, predictable way to temporarily alias hash
64             keys into lexical variables inside a coderef.
65             It is implemented using L and requires no XS, no parser hooks, and no syntax changes.
66              
67             =head1 FEATURES
68              
69             =head2 Read/write aliasing
70              
71             Lexicals declared in the outer scope become aliases to hash entries:
72              
73             my ($a);
74             with \%h, sub { $a = 10 }; # updates $h{a}
75              
76             =head2 Strict mode
77              
78             with -strict => \%h, sub { ... };
79              
80             Every valid hash key must have a matching lexical declared in the outer
81             scope. Missing lexicals cause an immediate error.
82              
83             =head2 Debug mode
84              
85             with -debug => \%h, sub { ... };
86              
87             Prints a summary of aliasing decisions:
88              
89             Aliased: $a -> %hash{a}
90             Ignored: foo-bar (invalid identifier)
91             Ignored: y (no lexical declared)
92              
93             =head2 Trace mode
94              
95             with -trace => \%h, sub { ... };
96              
97             Shows entry/exit and nesting depth:
98              
99             [with depth=1] entering with()
100             Aliased: $a -> %hash{a}
101             [with depth=1] leaving with()
102              
103             Trace mode implies debug mode.
104              
105             =head2 Nested with() support
106              
107             Nested calls work naturally:
108              
109             with \%h1, sub {
110             with \%h2, sub {
111             ...
112             };
113             };
114              
115             =head2 with_hash wrapper
116              
117             Syntactic sugar:
118              
119             with_hash %h => sub { ... };
120              
121             =head1 COOKBOOK
122              
123             This section provides practical,
124             ready-to.use patterns for common tasks
125             using C and C.
126             Each example is self-contained and demonstrates a specific technique or flag combination.
127              
128             =head2 Basic Aliasing
129              
130             Expose hash keys as lexicals inside a block:
131              
132             my %cfg = ( host => 'localhost', port => 3306 );
133             my ($host, $port);
134              
135             with_hash \%cfg, sub {
136             say "$host:$port"; # prints "localhost:3306"
137             $port = 3307; # updates %cfg
138             };
139              
140             =head2 Readonly Aliases
141              
142             Make lexicals read-only while still reflecting external changes:
143              
144             my %cfg = ( retries => 3 );
145             my ($retries);
146              
147             with_hash -readonly => \%cfg, sub {
148             say $retries; # ok
149             $retries++; # dies
150             };
151              
152             =head2 Strict Mode (lexicals must exist)
153              
154             Require that every aliased key has a declared lexical:
155              
156             my %cfg = ( host => 'localhost', port => 3306 );
157             my ($host, $port);
158              
159             with_hash -strict => \%cfg, sub {
160             say $host;
161             say $port;
162             };
163              
164             If a lexical is missing, C<-strict> throws an error before the block runs.
165              
166             =head2 Strict Keys (keys must have lexicals)
167              
168             Require that every visible key has a corresponding lexical:
169              
170             my %cfg = ( host => 'localhost', port => 3306 );
171             my ($host, $port);
172              
173             with_hash -strict_keys => \%cfg, sub {
174             () = $host; # ensure PadWalker sees it
175             () = $port;
176             };
177              
178             This is the inverse of C<-strict>.
179              
180             =head2 Renaming Keys
181              
182             Expose hash keys under different lexical names:
183              
184             my %cfg = ( 'http-status' => 200, 'user_id' => 42 );
185             my ($status, $user);
186              
187             with_hash
188             -rename => {
189             'http-status' => 'status',
190             'user_id' => 'user',
191             },
192             \%cfg,
193             sub {
194             say $status; # 200
195             say $user; # 42
196             };
197              
198             =head2 Filtering Keys with C<-only>
199              
200             Expose only a subset of keys:
201              
202             my %cfg = ( host => 'localhost', port => 3306, debug => 1 );
203             my ($host);
204              
205             with_hash
206             -only => [qw/host/],
207             \%cfg,
208             sub {
209             say $host; # ok
210             # $port and $debug are not aliased
211             };
212              
213             =head2 Filtering Keys with C<-except>
214              
215             Exclude specific keys:
216              
217             my %cfg = ( host => 'localhost', port => 3306, debug => 1 );
218             my ($host, $port);
219              
220             with_hash
221             -except => [qw/debug/],
222             \%cfg,
223             sub {
224             say $host; # ok
225             say $port; # ok
226             # $debug is not aliased
227             };
228              
229             =head2 Combining Filtering and Renaming
230              
231             Filtering happens first, then renaming:
232              
233             my %cfg = ( 'http-status' => 200, foo => 1, bar => 2 );
234             my ($status);
235              
236             with_hash
237             -only => [qw/http-status/],
238             -rename => { 'http-status' => 'status' },
239             \%cfg,
240             sub {
241             say $status; # 200
242             };
243              
244             =head2 Nested C Blocks
245              
246             Each block gets its own aliasing environment:
247              
248             my %outer = ( a => 1 );
249             my %inner = ( b => 2 );
250              
251             my ($a, $b);
252              
253             with_hash \%outer, sub {
254             say $a; # 1
255              
256             with_hash \%inner, sub {
257             say $b; # 2
258             };
259              
260             say $a; # still 1
261             };
262              
263             =head2 Using C Directly (Advanced)
264              
265             C is the low-level engine.
266             Use it when you already have a validated
267             hashref and want direct control:
268              
269             my %cfg = ( x => 10, y => 20 );
270             my ($x, $y);
271              
272             with \%cfg, sub {
273             $x += $y;
274             };
275              
276             =head2 Forcing PadWalker to See a Lexical
277              
278             PadWalker only reports lexicals that the coderef actually closes over.
279             To ensure a lexical is visible under C<-strict_keys>, use:
280              
281             () = $debug;
282              
283             This evaluates the variable in void context, ensuring that PadWalker
284             treats it as closed over without warnings.
285              
286             =cut
287              
288             # ------------------------------------------------------------
289             # with() — main entry point
290             # ------------------------------------------------------------
291             sub with {
292 52     52 1 1130520 my @args = @_;
293              
294 52         179 my %opts = (
295             # strict => 0,
296             # debug => 0,
297             # trace => 0,
298             # rename => undef,
299             # readonly => 0,
300             # strict_keys => 0,
301             );
302              
303 52   66     401 while (@args && $args[0] =~ /^-(strict|debug|trace|rename|readonly|strict_keys)$/) {
304 34         84 my $flag = shift @args;
305              
306 34 100       136 if ($flag eq '-rename') {
307 7         16 my $map = shift @args;
308 7 50       23 croak 'with(): -rename expects a hashref' unless ref($map) eq 'HASH';
309              
310 7         21 $opts{rename} = $map;
311 7         39 next;
312             }
313              
314 27         104 $flag =~ s/^-//;
315 27         153 $opts{$flag} = 1;
316             }
317              
318 52 100       172 $opts{debug} = 1 if $opts{trace};
319              
320 52         153 my ($href, $code) = @args;
321              
322 52 100       623 croak 'with(): first argument must be a hashref' unless ref($href) eq 'HASH';
323              
324 50 100       446 croak 'with(): second argument must be a coderef' unless ref($code) eq 'CODE';
325              
326 48         94 $WITH_DEPTH++;
327 48 100       150 warn "[with depth=$WITH_DEPTH] entering with()" if $opts{trace};
328              
329 48         301 my $closed = closed_over($code);
330 48         167 my %newpad = %$closed;
331              
332             # --------------------------------------------------------
333             # Process each hash key (aliasing)
334             # --------------------------------------------------------
335 48         151 KEY: for my $key (keys %$href) {
336             # Determine lexical name (after rename)
337 78 100 100     317 my $lex = $opts{rename} && exists $opts{rename}{$key} ? $opts{rename}{$key} : $key;
338              
339             # Skip invalid identifiers
340 78 100       374 unless ($lex =~ /^[A-Za-z_]\w*$/) {
341 8 100       37 warn "Ignored: $key (invalid identifier as $lex)" if $opts{debug};
342 8         24 next KEY;
343             }
344              
345 70         164 my $var = '$' . $lex;
346              
347             # strict_keys: every valid key must have a lexical
348 70 100 100     232 if ($opts{strict_keys} && !exists $newpad{$var}) {
349 3         59 die "with(): strict_keys mode: hash key '$key' has no lexical \$$lex";
350             }
351              
352             # strict: only keys that WOULD be aliased must have lexicals
353 67 100       165 unless (exists $newpad{$var}) {
354 8 100       29 if ($opts{strict}) {
355 3         62 die "with(): strict mode: lexical \$$lex not declared in outer scope";
356             }
357 5 50       15 warn "Ignored: $key (no lexical \$$lex declared)" if $opts{debug};
358 5         20 next KEY;
359             }
360              
361             # Alias
362 59 100       128 if ($opts{readonly}) {
363 4         34 tie my $ro, 'Syntax::Feature::With::ReadonlyScalar', \$href->{$key};
364 4         11 $newpad{$var} = \$ro;
365             } else {
366 55         126 $newpad{$var} = \$href->{$key};
367             }
368              
369 59 100       252 warn "Aliased: \$$lex => \%hash{$key}" if $opts{debug};
370             }
371              
372 42         192 set_closed_over($code, \%newpad);
373              
374 42         113 my $result = $code->();
375              
376 42 100       33256 warn "[with depth=$WITH_DEPTH] leaving with()" if $opts{trace};
377 42         89 $WITH_DEPTH--;
378              
379 42         204 return $result;
380             }
381              
382             =head2 with_hash
383              
384             with_hash \%hash, sub {
385             say $foo; # reads $hash{foo}
386             $bar = 123; # writes to $hash{bar}
387             };
388              
389             with_hash strict => a => 1, b => 2, sub {
390             ...
391             };
392              
393             Execute a block with temporary lexical aliases to the keys of a hash.
394              
395             C provides a convenient way to work with a hash by exposing each
396             key as a lexical variable inside a coderef. Reads and writes to those
397             lexicals operate directly on the underlying hash, making the block feel like
398             it has named parameters or local variables without the usual unpacking
399             boilerplate.
400              
401             This is syntactic sugar around C, normalizing the arguments and
402             ensuring that the hash and coderef are parsed correctly.
403              
404             =head3 Arguments
405              
406             C accepts the following forms:
407              
408             =over 4
409              
410             =item * Optional flags
411              
412             One or more strings that modify behaviour (e.g. C, C).
413             Flags must appear first.
414              
415             =item * A hash reference
416              
417             with_hash \%h, sub { ... };
418              
419             =item * A hash list
420              
421             with_hash a => 1, b => 2, sub { ... };
422              
423             The list must contain an even number of elements.
424              
425             When called with a key/value list rather than a hash reference,
426             C constructs an internal hash for the duration of the block.
427             Writes inside the block update this internal hash, not the caller's variables.
428              
429             =item * A final coderef (required)
430              
431             The last argument must be a coderef. It receives no parameters; instead,
432             lexical aliases are created for each hash key.
433              
434             =back
435              
436             =head3 Behaviour
437              
438             Inside the coderef:
439              
440             =over 4
441              
442             =item * Each hash key becomes a lexical variable
443              
444             $foo # alias to $hash{foo}
445             $bar # alias to $hash{bar}
446              
447             =item * Assigning to a lexical updates the original hash
448              
449             $foo = 42; # sets $hash{foo} = 42
450              
451             =item * Reading the lexical reads from the hash
452              
453             =item * Aliases are removed when the coderef returns
454              
455             =back
456              
457             =head3 Error Handling
458              
459             C throws descriptive exceptions when:
460              
461             =over 4
462              
463             =item * No coderef is provided
464              
465             =item * A hash list has an odd number of elements
466              
467             =item * Extra arguments appear after the coderef
468              
469             =item * The hash argument is neither a hashref nor a valid key/value list
470              
471             =back
472              
473             These errors are intended to catch common mistakes early and make test
474             failures easier to diagnose.
475              
476             =head3 Return Value
477              
478             Returns whatever the coderef returns.
479              
480             =head3 Examples
481              
482             Using a hashref:
483              
484             my %config = ( host => 'localhost', port => 3306 );
485              
486             with_hash \%config, sub {
487             say "$host:$port"; # prints "localhost:3306"
488             $port = 3307; # updates %config
489             };
490              
491             Using a hash list:
492              
493             with_hash debug => 1, retries => 3, sub {
494             $retries++; # modifies the underlying hash
495             };
496              
497             With flags:
498              
499             with_hash strict => \%opts, sub {
500             ...
501             };
502              
503             =head3 Notes
504              
505             C is intended for small, self-contained blocks where aliasing
506             improves clarity. It is not a general-purpose replacement for normal hash
507             access, nor does it attempt to provide full lexical scoping tricks beyond
508             simple aliasing.
509              
510             =head3 with vs. with_hash
511              
512             Although C and C share a similar calling style, they serve
513             different purposes and operate at different levels of abstraction.
514              
515             =head4 C - the low-level aliasing engine
516              
517             C is the core primitive. It expects:
518              
519             with \%hash, sub { ... };
520              
521             It assumes that:
522              
523             =over 4
524              
525             =item * The first argument is already a valid hash reference
526              
527             =item * The last argument is a coderef
528              
529             =item * Any flags have already been parsed
530              
531             =item * The hash keys are suitable for use as lexical variable names
532              
533             =back
534              
535             C performs no argument normalization. It simply creates lexical aliases
536             for each key in the provided hash and executes the coderef. It is strict,
537             minimal, and intended for internal use or advanced callers who want full
538             control.
539              
540             =head4 C - the user-friendly wrapper
541              
542             C is the public, ergonomic interface. It accepts a much more
543             flexible argument style:
544              
545             with_hash a => 1, b => 2, sub { ... };
546             with_hash \%hash, sub { ... };
547             with_hash strict => a => 1, b => 2, sub { ... };
548              
549             C is responsible for:
550              
551             =over 4
552              
553             =item * Parsing optional flags
554              
555             =item * Accepting either a hash reference OR a key/value list
556              
557             =item * Validating argument structure (even key/value pairs, final coderef, etc.)
558              
559             =item * Converting key/value lists into a hash reference
560              
561             =item * Producing clear, user-facing error messages
562              
563             =item * Calling C with a normalized hashref and the coderef
564              
565             =back
566              
567             In other words, C does all the DWIM work so that users can write
568             clean, concise code without worrying about argument shape or validation.
569              
570             =head4 Summary
571              
572             =over 4
573              
574             =item * Use C in normal code.
575              
576             =item * Use C only when you already have a validated hashref and want
577             direct access to the aliasing mechanism.
578              
579             =back
580              
581             C is the safe, friendly API.
582             C is the strict,
583             low-level engine that powers it.
584              
585             =head3 Key Filtering: C<-only> and C<-except>
586              
587             C supports two optional flags that control which keys from the
588             input hash are exposed as lexical aliases inside the block.
589              
590             These flags allow you to limit or refine the set of variables created,
591             making aliasing more intentional and avoiding namespace clutter.
592              
593             =head4 C<-only => \@keys>
594              
595             with_hash -only => [qw/foo bar/], \%hash, sub {
596             say $foo; # alias to $hash{foo}
597             say $bar; # alias to $hash{bar}
598             };
599              
600             Only the listed keys are aliased. Any keys not listed are ignored. Keys that
601             do not exist in the hash are silently skipped.
602              
603             =head4 C<-except => \@keys>
604              
605             with_hash -except => [qw/debug verbose/], \%hash, sub {
606             say $host; # ok
607             say $port; # ok
608             # $debug is NOT aliased
609             };
610              
611             All keys except those listed are aliased.
612              
613             =head4 Rules and Validation
614              
615             =over 4
616              
617             =item *
618              
619             C<-only> and C<-except> are mutually exclusive.
620             Using both at the same time results in an error.
621              
622             =item *
623              
624             Both flags require an array reference. Anything else triggers an error.
625              
626             =item *
627              
628             Filtering is applied B renaming or strict key validation.
629             Filtering temporarily hides keys from the underlying hash during the with() call.
630             Keys not selected by only/except are removed before aliasing and restored afterwards,
631             ensuring that write-through aliasing always affects the original hash.
632              
633             =item *
634              
635             If filtering removes all keys, the block still runs normally; no aliases are
636             created.
637              
638             =back
639              
640             =head4 Error Handling
641              
642             All validation errors are raised via C, so error messages correctly
643             report the caller's file and line number.
644              
645             =head3 -readonly
646              
647             with_hash -readonly => \%hash, sub {
648             say $foo; # ok
649             $foo = 10; # dies
650             };
651              
652             The C<-readonly> flag creates read-only aliases for each exposed hash key.
653             Reading works normally, but any attempt to assign to a lexical alias will
654             throw an exception.
655              
656             Readonly aliases still reflect changes made to the underlying hash from
657             outside the block.
658              
659             Readonly mode works with all other flags, including C<-rename>,
660             C<-only>, C<-except>, C<-strict>, and C<-trace>.
661              
662             =head3 -rename => { OLDKEY => NEWLEX, ... }
663              
664             The C<-rename> flag allows you to expose hash keys under different lexical
665             variable names inside the C block.
666              
667             This is useful when the original hash keys are not valid Perl identifiers
668             (e.g. contain hyphens), or when you want more convenient or descriptive
669             lexical names.
670              
671             with_hash
672             -rename => {
673             'http-status' => 'status',
674             'user_id' => 'user',
675             },
676             \%hash,
677             sub {
678             say $status; # alias to $hash{'http-status'}
679             say $user; # alias to $hash{'user_id'}
680             };
681              
682             Renaming does B copy values. The new lexical name is aliased directly
683             to the original hash slot, so write-through works as expected:
684              
685             $status = 404; # updates $hash{'http-status'}
686             $user = 99; # updates $hash{'user_id'}
687              
688             =head4 Interaction with filtering
689              
690             Renaming happens B C<-only> / C<-except> filtering. Filtering selects
691             which keys are visible; renaming changes the lexical names of those keys.
692              
693             For example:
694              
695             with_hash
696             -only => [qw/http-status foo/],
697             -rename => { 'http-status' => 'status' },
698             \%hash,
699             sub {
700             say $status; # ok
701             say $foo; # ok
702             say $user; # undef (not selected by -only)
703             };
704              
705             =head4 Interaction with strict mode
706              
707             When C<-strict> is enabled, every renamed lexical must be declared in the
708             outer scope. If a renamed lexical does not exist, C will croak:
709              
710             my ($status); # but NOT $missing_lex
711              
712             with_hash
713             -strict,
714             -rename => { 'http-status' => 'missing_lex' },
715             \%hash,
716             sub { ... };
717              
718             This dies with:
719              
720             strict mode: lexical $missing_lex not declared in outer scope
721              
722             =head4 Validity of new names
723              
724             The new lexical name must be a valid Perl identifier:
725              
726             /^[A-Za-z_]\w*$/
727              
728             If the new name is invalid, the key is ignored (or causes an error under
729             C<-strict>).
730              
731             =head4 Summary
732              
733             =over 4
734              
735             =item *
736             Renames hash keys to different lexical variable names.
737              
738             =item *
739             Write-through updates the original hash.
740              
741             =item *
742             Works with C<-only> and C<-except>.
743              
744             =item *
745             Respects C<-strict> (renamed lexicals must exist).
746              
747             =item *
748             Does not copy values; aliases directly to the original storage.
749              
750             =back
751              
752             =head3 -strict_keys
753              
754             with_hash -strict_keys => \%hash, sub { ... };
755              
756             The C<-strict_keys> flag enforces that every key in the input hash must have
757             a corresponding lexical variable declared in the outer scope. If any key is
758             missing a lexical, C will croak before executing the block.
759              
760             This is the inverse of C<-strict>, which enforces that every lexical must
761             correspond to a hash key.
762              
763             Strict key checking happens after filtering and renaming, so only the keys
764             that are actually exposed must be declared.
765              
766             my ($host, $port);
767              
768             with_hash
769             -strict_keys,
770             -rename => { host => 'h' },
771             \%config,
772             sub { ... };
773              
774             If C<%config> contains a key that does not map to a declared lexical (after
775             renaming), an error is thrown.
776              
777             This mode is useful for catching unexpected or misspelled keys in
778             configuration hashes or user input.
779              
780             =head4 A note on C<-strict_keys> and unused lexicals
781              
782             C<-strict_keys> relies on L to determine which
783             lexical variables are visible to the coderef. PadWalker only reports
784             lexicals that the coderef actually closes over. A lexical that is
785             declared in the outer scope but never referenced inside the block is
786             not considered "closed over" and therefore will not appear in the pad.
787              
788             This means that under C<-strict_keys>, a declared lexical must be
789             *mentioned* inside the block, otherwise it will be treated as missing:
790              
791             my ($host, $port, $debug);
792              
793             with_hash -strict_keys => \%cfg, sub {
794             say $host; # ok
795             # $port is declared but unused - PadWalker does not report it
796             # $debug is declared but unused - also not reported
797             };
798              
799             The above will die with:
800              
801             strict_keys mode: hash key 'port' has no lexical $port
802              
803             To force a lexical to be recognised without producing warnings, use the
804             standard idiom:
805              
806             () = $port;
807             () = $debug;
808              
809             This evaluates the variable in void context, ensuring that PadWalker
810             treats it as closed over, without affecting program behaviour.
811              
812             This is a limitation of Perl's closure model rather than of this module.
813              
814             =cut
815              
816             sub with_hash {
817 39     39 1 1294589 my @args = @_;
818              
819             # 1. Boolean flags
820 39         74 my @flags;
821 39   66     359 while (@args && $args[0] =~ /^-(strict|debug|trace|readonly|strict_keys)$/) {
822 23         132 push @flags, shift @args;
823             }
824              
825             # 2. Value-taking flags: -only, -except, -rename
826 39         74 my ($only, $except);
827              
828 39   66     196 while (@args && $args[0] =~ /^-(only|except|rename)$/) {
829 18         42 my $flag = shift @args;
830 18         37 my $value = shift @args;
831              
832 18 100 100     85 if ($flag eq '-only' || $flag eq '-except') {
833 11 100       53 croak "with_hash(): $flag expects an arrayref" unless ref($value) eq 'ARRAY';
834              
835 9 100       31 $only = $value if $flag eq '-only';
836 9 100       39 $except = $value if $flag eq '-except';
837 9         55 next;
838             }
839              
840 7 50       21 if ($flag eq '-rename') {
841 7 50       59 croak "with_hash(): -rename expects a hashref" unless ref($value) eq 'HASH';
842              
843 7         20 push @flags, ($flag, $value);
844 7         47 next;
845             }
846             }
847              
848 37 100 100     131 croak 'with_hash(): cannot use both -only and -except' if $only && $except;
849              
850             # 3. Extract coderef
851 36 50       83 croak 'with_hash(): missing coderef' unless @args;
852              
853 36         71 my $code = pop @args;
854              
855 36 100       179 croak 'with_hash(): last argument must be a coderef' unless ref($code) eq 'CODE';
856              
857             # 4. Normalize hash argument
858 35         67 my $href;
859              
860 35 100 66     150 if (@args == 1 && ref($args[0]) eq 'HASH') {
861 26         89 $href = shift @args;
862             } else {
863 9 100 100     29 if (@args >= 1 && ref($args[0]) eq 'HASH') {
864 1         89 croak 'with_hash(): hashref must be the only argument before coderef';
865             }
866              
867 8 100       183 croak 'with_hash(): odd number of elements in hash list' if @args % 2;
868              
869 7         18 my %h = @args;
870 7         13 $href = \%h;
871             }
872              
873             # 5. Filtering (delete/restore)
874 33         60 my %removed;
875              
876 33 100 100     141 if ($only || $except) {
877 7 100       27 my %only = $only ? map { $_ => 1 } @$only : ();
  8         38  
878 7 100       25 my %except = $except ? map { $_ => 1 } @$except : ();
  3         15  
879              
880 7         11 my %keep;
881 7 100       21 if ($only) {
    50          
882 5         19 %keep = %only;
883             } elsif ($except) {
884 2         8 %keep = map { $_ => 1 } grep { !$except{$_} } keys %$href;
  5         19  
  7         14  
885             }
886              
887 7         22 for my $k (keys %$href) {
888 26 100       64 next if $keep{$k};
889 13         27 $removed{$k} = $href->{$k};
890 13         23 delete $href->{$k};
891             }
892             }
893              
894             # 6. Call underlying engine — FLAGS FIRST
895 33         101 my $result = with(@flags, $href, $code);
896              
897             # 7. Restore removed keys
898 28 100       104 @$href{keys %removed} = values %removed if %removed;
899              
900 28         136 return $result;
901             }
902              
903             1;
904              
905             package Syntax::Feature::With::ReadonlyScalar;
906              
907 13     13   123 use Carp 'croak';
  13         1122  
  13         3017  
908              
909             sub TIESCALAR {
910 4     4   12 my ($class, $ref) = @_;
911 4         22 return bless { ref => $ref }, $class;
912             }
913              
914             sub FETCH {
915 3     3   147 my $self = $_[0];
916 3         28 return ${ $self->{ref} };
  3         26  
917             }
918              
919             sub STORE {
920 3     3   3739 my ($self, $value) = @_;
921 3         73 croak 'with(): readonly variable cannot be modified';
922             }
923              
924             1;
925              
926             __END__