Skip to content

Commit 892ae94

Browse files
authored
Merge pull request #322 from fglock/fix/mo-bareword-parsing
Fix ::identifier bareword parsing and jcpan cpan script
2 parents 29f1a21 + 3e75045 commit 892ae94

6 files changed

Lines changed: 435 additions & 31 deletions

File tree

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ target/
1313
.java-version
1414
build/
1515
bin/
16+
# But allow Perl bin scripts
17+
!src/main/perl/bin/
1618
gradle/*
1719
!gradle/libs.versions.toml
1820
gradlew

dev/design/moo_support.md

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -589,11 +589,34 @@ Moo tests run via `jcpan -t Moo`. Recent fixes (Phases 12-13) should improve pas
589589
- This fixes Sub::Quote's `local @_ = ($value)` inlinification pattern
590590
- Fixes t/method-generate-accessor.t (46/49 → 49/49)
591591
592+
- [x] Phase 24: Fix ::identifier bareword parsing (2026-03-16)
593+
- Root cause: `::foo` without parens was always treated as `main::foo` identifier
594+
- In Perl: `::foo` calls main::foo only if sub exists at compile time, else bareword string
595+
- Mo uses `$M.$_.::e` to build package names - `::e` should be bareword string
596+
- But tests use `::is ::exception { }` where `is` and `exception` are imported subs
597+
- **ParsePrimary.java fix**:
598+
- Check `GlobalVariable.getGlobalCodeRef(fullSubName).getDefinedBoolean()`
599+
- If sub exists OR followed by `(`: function call (main::identifier)
600+
- If sub doesn't exist AND no parens: bareword string ('::identifier')
601+
- **config.yaml fix**: Added cpan script to sync config for jcpan wrapper
602+
- **.gitignore fix**: Allow src/main/perl/bin/ directory in git
603+
- Mo tests: 27/28 passing (99.3%)
604+
605+
- [x] Phase 25: Fix self-referential hash assignment (2026-03-16)
606+
- Root cause: `%h = (new_stuff, %h)` was clearing hash before evaluating `%h`
607+
- Mo uses: `%e = (extends => sub{...}, has => sub{...}, %e)` to merge exports
608+
- The hash was cleared before iterating over the RHS list containing `%h`
609+
- **RuntimeHash.java fix**:
610+
- Materialize entire RHS list into temporary array BEFORE clearing hash
611+
- Similar to how tied hashes are already handled
612+
- This fixed Mo's BUILD feature which depends on the %e merge pattern
613+
- Mo tests: 6/28 failing → 1/28 failing (143/144 subtests pass)
614+
592615
### Current Status
593616
594-
**Test Results (after Phase 23):**
595-
- 62/71 test programs passing (87%)
596-
- ~768/829 subtests passing (93%)
617+
**Test Results (after Phase 25):**
618+
- **Moo**: 62/71 test programs passing (87%), 768/829 subtests passing (93%)
619+
- **Mo**: 27/28 test programs passing (99.3%), 143/144 subtests passing
597620
598621
**Remaining Failures (categorized):**
599622
1. **accessor-weaken tests** (20 failures) - Expected, weak references not supported in Java GC
@@ -602,6 +625,7 @@ Moo tests run via `jcpan -t Moo`. Recent fixes (Phases 12-13) should improve pas
602625
4. **moo-utils-_subname-Sub-Name.t** (1 failure) - Expected, we have Sub::Util (no fallback to Sub::Name)
603626
5. **no-moo.t** (5 failures) - Namespace cleanup requires weak references
604627
6. **overloaded-coderefs.t** - Expected, B::Deparse not available
628+
7. **Mo t/strict.t** (1 failure) - Error message format differs from Perl
605629
606630
**Expected failures** (not fixable without fundamental changes):
607631
- Weak references: accessor-weaken tests (20), no-moo.t cleanup (5)
@@ -621,12 +645,16 @@ Moo tests run via `jcpan -t Moo`. Recent fixes (Phases 12-13) should improve pas
621645
### PR Information
622646
- **Branch**: `feature/moo-support` (PR #319 - merged)
623647
- **Branch**: `fix/goto-tailcall-import` (PR #320 - open)
648+
- **Branch**: `fix/mo-bareword-parsing` (PR #322 - open)
624649
- **Key commits**:
625650
- `00c124167` - Fix print { func() } filehandle block parsing and JVM codegen
626651
- `393bedf0f` - Fix quotemeta and Package::SUPER::method resolution
627652
- `7a76739b8` - Fix goto &sub in use/import TAILCALL handling
628653
- `053d91a95` - Add Sub::Util, fix Scalar/List::Util VERSION, add Test::Harness
629654
- `7993ef74d` - Fix version parsing and MM->parse_version for CPAN.pm
655+
- `db434f8d3` - Fix ::identifier bareword parsing and add cpan to sync
656+
- `ff31163f9` - Fix self-referential hash assignment %h = (stuff, %h)
657+
- `a3233cd55` - Improve ::identifier to check sub existence at compile time
630658
631659
## Related Documents
632660

dev/import-perl5/config.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ imports:
2727
- source: perl5/dist/Carp/lib/Carp/Heavy.pm
2828
target: src/main/perl/lib/Carp/Heavy.pm
2929

30+
# CPAN client script - required by jcpan wrapper
31+
- source: perl5/cpan/CPAN/scripts/cpan
32+
target: src/main/perl/bin/cpan
33+
3034
- source: perl5/lib/Benchmark.pm
3135
target: src/main/perl/lib/Benchmark.pm
3236

src/main/java/org/perlonjava/frontend/parser/ParsePrimary.java

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -269,15 +269,42 @@ static Node parseOperator(Parser parser, LexerToken token, String operator) {
269269
return StringParser.parseRawString(parser, token.text);
270270

271271
case "::":
272-
// Leading :: means main:: (e.g., ::foo is main::foo)
273-
// This allows accessing global variables even when a lexical exists
272+
// Leading :: can mean either:
273+
// 1. Function call: ::foo() -> main::foo()
274+
// 2. Bareword reference: ::foo without parens -> calls main::foo if it exists
275+
// 3. Bareword string: ::foo when no such sub exists -> '::foo'
276+
//
277+
// In Perl, ::foo without parens calls main::foo if the sub exists at compile time.
278+
// If no such sub exists, it becomes a bareword string '::foo'.
274279
LexerToken nextToken2 = peek(parser);
275280
if (nextToken2.type == LexerTokenType.IDENTIFIER) {
276-
// Insert "main" before the :: to create main::identifier
277-
parser.tokens.add(parser.tokenIndex - 1, new LexerToken(LexerTokenType.IDENTIFIER, "main"));
278-
parser.tokenIndex--; // Go back to process "main"
279-
return parseIdentifier(parser, parser.tokenIndex,
280-
new LexerToken(LexerTokenType.IDENTIFIER, "main"), "main");
281+
String identifierName = nextToken2.text;
282+
// Look ahead to see if this is a function call with parens
283+
int lookAhead = parser.tokenIndex + 1;
284+
// Skip whitespace
285+
while (lookAhead < parser.tokens.size() &&
286+
parser.tokens.get(lookAhead).type == LexerTokenType.WHITESPACE) {
287+
lookAhead++;
288+
}
289+
String afterIdentifier = lookAhead < parser.tokens.size() ?
290+
parser.tokens.get(lookAhead).text : "";
291+
292+
// Check if the sub exists at compile time
293+
String fullSubName = "main::" + identifierName;
294+
boolean subExists = GlobalVariable.getGlobalCodeRef(fullSubName).getDefinedBoolean();
295+
296+
if (afterIdentifier.equals("(") || subExists) {
297+
// Function call: ::foo() or ::foo (when sub exists)
298+
// Insert "main" before the :: to create main::identifier
299+
parser.tokens.add(parser.tokenIndex - 1, new LexerToken(LexerTokenType.IDENTIFIER, "main"));
300+
parser.tokenIndex--; // Go back to process "main"
301+
return parseIdentifier(parser, parser.tokenIndex,
302+
new LexerToken(LexerTokenType.IDENTIFIER, "main"), "main");
303+
} else {
304+
// Bareword: ::foo -> '::foo' (no such sub exists)
305+
parser.tokenIndex++; // Consume the identifier
306+
return new StringNode("::" + identifierName, parser.tokenIndex);
307+
}
281308
}
282309
throw new PerlCompilerException(parser.tokenIndex, "syntax error", parser.ctx.errorUtil);
283310

src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeHash.java

Lines changed: 15 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -189,23 +189,18 @@ public RuntimeScalar addToScalar(RuntimeScalar scalar) {
189189
public RuntimeArray setFromList(RuntimeList value) {
190190
return switch (type) {
191191
case PLAIN_HASH -> {
192-
// Store the original list size for scalar context
193-
int originalSize = 0;
194-
for (RuntimeBase elem : value.elements) {
195-
if (elem instanceof RuntimeArray) {
196-
originalSize += ((RuntimeArray) elem).elements.size();
197-
} else if (elem instanceof RuntimeScalar) {
198-
originalSize++;
199-
} else {
200-
// Count elements by iterating
201-
Iterator<RuntimeScalar> it = elem.iterator();
202-
while (it.hasNext()) {
203-
it.next();
204-
originalSize++;
205-
}
206-
}
192+
// First, fully materialize the right-hand side list BEFORE clearing
193+
// This is critical for self-referential assignments like: %h = (new_stuff, %h)
194+
// We must capture the current hash contents before clearing.
195+
RuntimeArray materializedList = new RuntimeArray();
196+
Iterator<RuntimeScalar> iterator = value.iterator();
197+
while (iterator.hasNext()) {
198+
materializedList.push(new RuntimeScalar(iterator.next()));
207199
}
208200

201+
// Store the original list size for scalar context
202+
int originalSize = materializedList.elements.size();
203+
209204
// Warn about odd elements (Perl does not warn about references in hash assignment)
210205
if (originalSize % 2 != 0) {
211206
WarnDie.warn(
@@ -216,13 +211,12 @@ public RuntimeArray setFromList(RuntimeList value) {
216211
// Clear existing elements but keep the same Map instance to preserve capacity
217212
this.elements.clear();
218213

219-
// Populate the hash from the provided list
220-
// This reuses the existing StableHashMap and its capacity
221-
Iterator<RuntimeScalar> iter = value.iterator();
222-
while (iter.hasNext()) {
223-
String key = iter.next().toString();
214+
// Populate the hash from the materialized list
215+
iterator = materializedList.iterator();
216+
while (iterator.hasNext()) {
217+
String key = iterator.next().toString();
224218
// Create a new RuntimeScalar to properly handle aliasing and avoid read-only issues
225-
RuntimeScalar val = iter.hasNext() ? new RuntimeScalar(iter.next()) : new RuntimeScalar();
219+
RuntimeScalar val = iterator.hasNext() ? new RuntimeScalar(iterator.next()) : new RuntimeScalar();
226220
this.elements.put(key, val);
227221
}
228222

0 commit comments

Comments
 (0)