2121import static org .perlonjava .runtime .GlobalVariable .getGlobalVariable ;
2222import static org .perlonjava .runtime .RuntimeScalarCache .*;
2323
24+ /**
25+ * ModuleOperators implements Perl's module loading operators: `do`, `require`, and `use`.
26+ *
27+ * <p>This class handles multiple forms of code loading:
28+ * <ul>
29+ * <li><b>do FILE</b> - Executes a file without checking %INC</li>
30+ * <li><b>do \&coderef</b> - Executes code reference as @INC filter (generator pattern)</li>
31+ * <li><b>do [\&coderef, state...]</b> - @INC filter with state parameters</li>
32+ * <li><b>do $filehandle</b> - Reads and executes from filehandle</li>
33+ * <li><b>do [$filehandle, \&filter, state...]</b> - Filehandle with filter chain</li>
34+ * <li><b>require FILE</b> - Loads module once, checks %INC, requires true value</li>
35+ * <li><b>require VERSION</b> - Version checking</li>
36+ * </ul>
37+ *
38+ * <h2>@INC Filter Support</h2>
39+ * <p>When a code reference is passed to `do`, it's called repeatedly as a generator:
40+ * <ul>
41+ * <li>Each call should populate $_ with a chunk of code</li>
42+ * <li>Return 0/false to signal EOF</li>
43+ * <li>Return true to continue reading</li>
44+ * <li>Optional state parameters are passed as @_ (starting at $_[1])</li>
45+ * </ul>
46+ *
47+ * <h2>Error Handling</h2>
48+ * <p>Errors are stored in special variables:
49+ * <ul>
50+ * <li><b>$@</b> - Compilation/execution errors</li>
51+ * <li><b>$!</b> - I/O errors (file not found, permissions, etc.)</li>
52+ * </ul>
53+ *
54+ * @see <a href="https://perldoc.perl.org/functions/do">perldoc do</a>
55+ * @see <a href="https://perldoc.perl.org/functions/require">perldoc require</a>
56+ */
2457public class ModuleOperators {
58+
59+ /**
60+ * Public entry point for `do` operator.
61+ *
62+ * <p>Always sets %INC and keeps the entry regardless of execution result.
63+ * This differs from `require` which removes %INC entries on failure.
64+ *
65+ * @param runtimeScalar The file, coderef, filehandle, or array reference to execute
66+ * @param ctx Execution context (scalar or list)
67+ * @return Result of execution (undef on error)
68+ */
2569 public static RuntimeBase doFile (RuntimeScalar runtimeScalar , int ctx ) {
2670 return doFile (runtimeScalar , true , false , ctx ); // do FILE always sets %INC and keeps it
2771 }
2872
73+ /**
74+ * Internal implementation of `do` and `require` operators.
75+ *
76+ * <p>This method handles the complex dispatch logic for different argument types:
77+ *
78+ * <h3>1. Array Reference: [\&coderef, state...]</h3>
79+ * <p>When the first element is a code reference:
80+ * <ul>
81+ * <li>Extract the coderef from array[0]</li>
82+ * <li>Pass array[1..N] as state parameters to the coderef</li>
83+ * <li>Call coderef repeatedly until it returns false</li>
84+ * <li>Each call populates $_ with code chunks</li>
85+ * </ul>
86+ *
87+ * <h3>2. Code Reference: \&generator</h3>
88+ * <p>When a coderef is passed directly:
89+ * <ul>
90+ * <li>Call repeatedly as generator (no state parameters)</li>
91+ * <li>Each call should set $_ to next chunk</li>
92+ * <li>Return false to signal EOF</li>
93+ * </ul>
94+ *
95+ * <h3>3. Filehandle: $fh</h3>
96+ * <p>Read entire contents from filehandle and execute.
97+ *
98+ * <h3>4. Filename: "Module/Name.pm"</h3>
99+ * <p>Standard file loading:
100+ * <ul>
101+ * <li>Search @INC directories</li>
102+ * <li>Check for .pmc (compiled) version first</li>
103+ * <li>Read file and execute</li>
104+ * </ul>
105+ *
106+ * @param runtimeScalar The argument to do/require
107+ * @param setINC Whether to set %INC entry for this file
108+ * @param isRequire True if called from require (affects %INC cleanup on failure)
109+ * @param ctx Execution context (scalar or list)
110+ * @return Result of execution (undef on error, with $@ or $! set)
111+ */
29112 private static RuntimeBase doFile (RuntimeScalar runtimeScalar , boolean setINC , boolean isRequire , int ctx ) {
30113 // Clear error variables at start
31114 GlobalVariable .setGlobalVariable ("main::@" , "" );
@@ -36,41 +119,114 @@ private static RuntimeBase doFile(RuntimeScalar runtimeScalar, boolean setINC, b
36119 String code = null ;
37120 String actualFileName = null ;
38121
39- // Check if the argument is an ARRAY reference (for @INC filter with state support)
40- if (runtimeScalar .type == RuntimeScalarType .REFERENCE &&
122+ // Variables for handling array references with state
123+ RuntimeCode codeRef = null ;
124+ RuntimeArray stateArgs = null ;
125+
126+ // ===== STEP 1: Handle ARRAY reference =====
127+ // Array format: [coderef|filehandle, state...]
128+ if (runtimeScalar .type == RuntimeScalarType .ARRAYREFERENCE &&
41129 runtimeScalar .value instanceof RuntimeArray ) {
42- // `do` ARRAY reference - array should contain [coderef, state...]
43130 RuntimeArray arr = (RuntimeArray ) runtimeScalar .value ;
44131 if (arr .size () > 0 ) {
45132 RuntimeScalar firstElem = arr .get (0 );
46- // The first element should be a CODE ref or filehandle
133+
134+ // Case 1a: Array with CODE reference [&coderef, state...]
135+ // Extract coderef and state parameters for later execution
47136 if (firstElem .type == RuntimeScalarType .CODE ||
48137 (firstElem .type == RuntimeScalarType .REFERENCE &&
49138 firstElem .scalarDeref () != null &&
50- firstElem .scalarDeref ().type == RuntimeScalarType .CODE ) ||
51- firstElem .type == RuntimeScalarType .GLOB ||
139+ firstElem .scalarDeref ().type == RuntimeScalarType .CODE )) {
140+ // Extract the coderef from first element
141+ if (firstElem .type == RuntimeScalarType .CODE ) {
142+ codeRef = (RuntimeCode ) firstElem .value ;
143+ } else if (firstElem .value instanceof RuntimeCode ) {
144+ codeRef = (RuntimeCode ) firstElem .value ;
145+ } else {
146+ RuntimeScalar deref = firstElem .scalarDeref ();
147+ if (deref != null && deref .value instanceof RuntimeCode ) {
148+ codeRef = (RuntimeCode ) deref .value ;
149+ }
150+ }
151+
152+ // Create arguments array from remaining elements (state parameters)
153+ // These will be passed as @_ to the coderef
154+ // Note: $_[0] is reserved for filename (undef for generators), state starts at $_[1]
155+ stateArgs = new RuntimeArray ();
156+ stateArgs .push (new RuntimeScalar ()); // $_[0] = undef (filename placeholder)
157+ for (int i = 1 ; i < arr .size (); i ++) {
158+ stateArgs .push (arr .get (i )); // $_[1..N] = state parameters
159+ }
160+ // Fall through to CODE handling below
161+ }
162+ // Case 1b: Array with filehandle [$fh, &filter, state...]
163+ // Read from filehandle, apply filter if present, then execute
164+ else if (firstElem .type == RuntimeScalarType .GLOB ||
52165 firstElem .type == RuntimeScalarType .GLOBREFERENCE ) {
53- // Recursively handle the first element
54- // Pass remaining array elements as potential state
55- RuntimeBase result = doFile (firstElem , setINC , isRequire , ctx );
56- return result ;
166+ // Read content from filehandle
167+ code = Readline .readline (firstElem , RuntimeContextType .LIST ).toString ();
168+
169+ // Check if there's a filter (second element)
170+ if (arr .size () > 1 ) {
171+ RuntimeScalar secondElem = arr .get (1 );
172+ if (secondElem .type == RuntimeScalarType .CODE ||
173+ (secondElem .type == RuntimeScalarType .REFERENCE &&
174+ secondElem .scalarDeref () != null &&
175+ secondElem .scalarDeref ().type == RuntimeScalarType .CODE )) {
176+ // Extract filter coderef
177+ RuntimeCode filterRef = null ;
178+ if (secondElem .type == RuntimeScalarType .CODE ) {
179+ filterRef = (RuntimeCode ) secondElem .value ;
180+ } else if (secondElem .value instanceof RuntimeCode ) {
181+ filterRef = (RuntimeCode ) secondElem .value ;
182+ } else {
183+ RuntimeScalar deref = secondElem .scalarDeref ();
184+ if (deref != null && deref .value instanceof RuntimeCode ) {
185+ filterRef = (RuntimeCode ) deref .value ;
186+ }
187+ }
188+
189+ if (filterRef != null ) {
190+ // Apply filter to the content
191+ RuntimeScalar savedDefaultVar = GlobalVariable .getGlobalVariable ("main::_" );
192+ try {
193+ // Set $_ to the content
194+ GlobalVariable .getGlobalVariable ("main::_" ).set (code );
195+
196+ // Build filter args: $_[0] = undef, $_[1..N] = state
197+ RuntimeArray filterArgs = new RuntimeArray ();
198+ filterArgs .push (new RuntimeScalar ()); // $_[0] = undef
199+ for (int i = 2 ; i < arr .size (); i ++) {
200+ filterArgs .push (arr .get (i )); // $_[1..N] = state
201+ }
202+
203+ // Call the filter
204+ filterRef .apply (filterArgs , RuntimeContextType .SCALAR );
205+
206+ // Get modified content from $_
207+ code = GlobalVariable .getGlobalVariable ("main::_" ).toString ();
208+ } finally {
209+ // Restore $_
210+ GlobalVariable .getGlobalVariable ("main::_" ).set (savedDefaultVar .toString ());
211+ }
212+ }
213+ }
214+ }
215+ // Continue to execution phase with the (possibly filtered) code
57216 }
58217 }
59218 }
60- // Check if the argument is a CODE reference (for @INC filter support)
61- else if (runtimeScalar .type == RuntimeScalarType .CODE ||
219+
220+ // ===== STEP 2: Handle direct CODE reference =====
221+ // Check if the argument is a CODE reference (not already extracted from array)
222+ if (codeRef == null && (runtimeScalar .type == RuntimeScalarType .CODE ||
62223 (runtimeScalar .type == RuntimeScalarType .REFERENCE &&
63224 runtimeScalar .scalarDeref () != null &&
64- runtimeScalar .scalarDeref ().type == RuntimeScalarType .CODE )) {
65- // `do` CODE reference - execute the subroutine as an @INC filter
66- // The subroutine should populate $_ with file content
67- // Return value of 0 means EOF
68-
69- RuntimeCode codeRef = null ;
225+ runtimeScalar .scalarDeref ().type == RuntimeScalarType .CODE ))) {
226+ // Extract the coderef
70227 if (runtimeScalar .type == RuntimeScalarType .CODE ) {
71228 codeRef = (RuntimeCode ) runtimeScalar .value ;
72229 } else {
73- // For REFERENCE type, the value is already the RuntimeCode
74230 if (runtimeScalar .value instanceof RuntimeCode ) {
75231 codeRef = (RuntimeCode ) runtimeScalar .value ;
76232 } else {
@@ -80,60 +236,69 @@ else if (runtimeScalar.type == RuntimeScalarType.CODE ||
80236 }
81237 }
82238 }
239+
240+ // Create args with filename placeholder if not already set (no state for direct coderef)
241+ if (stateArgs == null ) {
242+ stateArgs = new RuntimeArray ();
243+ stateArgs .push (new RuntimeScalar ()); // $_[0] = undef (filename placeholder)
244+ }
245+ }
83246
84- if (codeRef == null ) {
85- // Not a valid CODE reference
86- code = null ;
87- } else {
88- // Save current $_
89- RuntimeScalar savedDefaultVar = GlobalVariable .getGlobalVariable ("main::_" );
90- StringBuilder accumulatedCode = new StringBuilder ();
91-
92- try {
93- // Call the CODE reference repeatedly as a generator
94- // Each call should populate $_ with the next chunk of code
95- // Continue until it returns 0/false (EOF)
96- RuntimeArray args = new RuntimeArray ();
97- boolean continueReading = true ;
247+ // ===== STEP 3: Execute CODE reference as generator =====
248+ // This handles both array-extracted and direct code references
249+ if (codeRef != null ) {
250+ RuntimeScalar savedDefaultVar = GlobalVariable .getGlobalVariable ("main::_" );
251+ StringBuilder accumulatedCode = new StringBuilder ();
252+
253+ try {
254+ // Generator pattern: call repeatedly until false is returned
255+ // Each call should populate $_ with a chunk of code
256+ // State parameters (if any) are passed as @_
257+ boolean continueReading = true ;
258+
259+ while (continueReading ) {
260+ // Clear $_ before each call
261+ GlobalVariable .getGlobalVariable ("main::_" ).set ("" );
98262
99- while (continueReading ) {
100- GlobalVariable .getGlobalVariable ("main::_" ).set ("" );
101-
102- // Call the CODE reference
103- RuntimeBase result = codeRef .apply (args , RuntimeContextType .SCALAR );
104-
105- // Get the content from $_
106- RuntimeScalar defaultVar = GlobalVariable .getGlobalVariable ("main::_" );
107- String chunk = defaultVar .toString ();
108-
109- // Accumulate the chunk if not empty
110- if (!chunk .isEmpty ()) {
111- accumulatedCode .append (chunk );
112- }
113-
114- // Check if we should continue
115- // Return value of 0/false means EOF
116- continueReading = result .scalar ().getBoolean ();
117- }
263+ // Call the CODE reference with state arguments
264+ // The coderef should populate $_ with content
265+ RuntimeBase result = codeRef .apply (stateArgs , RuntimeContextType .SCALAR );
118266
119- code = accumulatedCode .toString ();
120- if (code .isEmpty ()) {
121- code = null ;
267+ // Get the content from $_
268+ RuntimeScalar defaultVar = GlobalVariable .getGlobalVariable ("main::_" );
269+ String chunk = defaultVar .toString ();
270+
271+ // Accumulate the chunk if not empty
272+ if (!chunk .isEmpty ()) {
273+ accumulatedCode .append (chunk );
122274 }
123- } catch (Exception e ) {
124- // If there's an error executing the CODE ref, treat as no content
275+
276+ // Check if we should continue
277+ // Return value of 0/false means EOF
278+ continueReading = result .scalar ().getBoolean ();
279+ }
280+
281+ code = accumulatedCode .toString ();
282+ if (code .isEmpty ()) {
125283 code = null ;
126- throw e ; // Re-throw to maintain error handling
127- } finally {
128- // Restore $_
129- GlobalVariable .getGlobalVariable ("main::_" ).set (savedDefaultVar .toString ());
130284 }
285+ } catch (Exception e ) {
286+ // If there's an error executing the CODE ref, treat as no content
287+ code = null ;
288+ throw e ; // Re-throw to maintain error handling
289+ } finally {
290+ // Restore $_ to its previous value
291+ GlobalVariable .getGlobalVariable ("main::_" ).set (savedDefaultVar .toString ());
131292 }
132- } else if (runtimeScalar .type == RuntimeScalarType .GLOB || runtimeScalar .type == RuntimeScalarType .GLOBREFERENCE ) {
133- // `do` filehandle
293+ }
294+ // ===== STEP 4: Handle filehandle =====
295+ else if (runtimeScalar .type == RuntimeScalarType .GLOB || runtimeScalar .type == RuntimeScalarType .GLOBREFERENCE ) {
296+ // Read entire contents from filehandle
134297 code = Readline .readline (runtimeScalar , RuntimeContextType .LIST ).toString ();
135- } else {
136- // `do` filename
298+ }
299+ // ===== STEP 5: Handle filename (standard file loading) =====
300+ // Only process as filename if code hasn't been set yet
301+ else if (code == null ) {
137302
138303 // Check if the filename is an absolute path or starts with ./ or ../
139304 Path filePath = Paths .get (fileName );
@@ -290,9 +455,49 @@ else if (runtimeScalar.type == RuntimeScalarType.CODE ||
290455 }
291456 }
292457
458+ /**
459+ * Implements Perl's `require` operator.
460+ *
461+ * <p>The `require` operator has two distinct behaviors:
462+ *
463+ * <h3>1. Version Checking: require VERSION</h3>
464+ * <p>When given a numeric or vstring value:
465+ * <ul>
466+ * <li>Compare against current Perl version</li>
467+ * <li>Throw exception if version requirement not met</li>
468+ * <li>Return 1 if version is sufficient</li>
469+ * </ul>
470+ *
471+ * <h3>2. Module Loading: require MODULE</h3>
472+ * <p>When given a string (module name or filename):
473+ * <ul>
474+ * <li>Check if already loaded in %INC (return 1 if so)</li>
475+ * <li>Search @INC directories for the file</li>
476+ * <li>Compile and execute the file</li>
477+ * <li>Require that execution returns a true value</li>
478+ * <li>Add entry to %INC on success</li>
479+ * <li>Remove from %INC or mark as undef on failure</li>
480+ * </ul>
481+ *
482+ * <h3>Error Handling</h3>
483+ * <p>`require` is stricter than `do`:
484+ * <ul>
485+ * <li>Throws exception if file not found</li>
486+ * <li>Throws exception if compilation fails</li>
487+ * <li>Throws exception if module returns false value</li>
488+ * <li>Marks compilation failures as undef in %INC (cached failure)</li>
489+ * </ul>
490+ *
491+ * @param runtimeScalar Module name, filename, or version to require
492+ * @return Always returns 1 on success (or throws exception)
493+ * @throws PerlCompilerException if version insufficient, file not found,
494+ * compilation fails, or module returns false
495+ * @see <a href="https://perldoc.perl.org/functions/require">perldoc require</a>
496+ */
293497 public static RuntimeScalar require (RuntimeScalar runtimeScalar ) {
294498 // https://perldoc.perl.org/functions/require
295499
500+ // ===== CASE 1: Version checking =====
296501 if (runtimeScalar .type == RuntimeScalarType .INTEGER || runtimeScalar .type == RuntimeScalarType .DOUBLE || runtimeScalar .type == RuntimeScalarType .VSTRING || runtimeScalar .type == RuntimeScalarType .BOOLEAN ) {
297502 // `require VERSION` - use version comparison
298503 String currentVersionStr = Configuration .perlVersion ;
0 commit comments