Skip to content

Commit 2444dec

Browse files
authored
Merge pull request #41 from fglock/fix-incfilter-do-operator
Fix do operator array reference handling and add comprehensive docume…
2 parents ca88698 + e165c6d commit 2444dec

File tree

1 file changed

+270
-65
lines changed

1 file changed

+270
-65
lines changed

src/main/java/org/perlonjava/operators/ModuleOperators.java

Lines changed: 270 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,94 @@
2121
import static org.perlonjava.runtime.GlobalVariable.getGlobalVariable;
2222
import 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+
*/
2457
public 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

Comments
 (0)