Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ target/
.java-version
build/
bin/
# But allow Perl bin scripts
!src/main/perl/bin/
gradle/*
!gradle/libs.versions.toml
gradlew
Expand Down
34 changes: 31 additions & 3 deletions dev/design/moo_support.md
Original file line number Diff line number Diff line change
Expand Up @@ -589,11 +589,34 @@ Moo tests run via `jcpan -t Moo`. Recent fixes (Phases 12-13) should improve pas
- This fixes Sub::Quote's `local @_ = ($value)` inlinification pattern
- Fixes t/method-generate-accessor.t (46/49 → 49/49)

- [x] Phase 24: Fix ::identifier bareword parsing (2026-03-16)
- Root cause: `::foo` without parens was always treated as `main::foo` identifier
- In Perl: `::foo` calls main::foo only if sub exists at compile time, else bareword string
- Mo uses `$M.$_.::e` to build package names - `::e` should be bareword string
- But tests use `::is ::exception { }` where `is` and `exception` are imported subs
- **ParsePrimary.java fix**:
- Check `GlobalVariable.getGlobalCodeRef(fullSubName).getDefinedBoolean()`
- If sub exists OR followed by `(`: function call (main::identifier)
- If sub doesn't exist AND no parens: bareword string ('::identifier')
- **config.yaml fix**: Added cpan script to sync config for jcpan wrapper
- **.gitignore fix**: Allow src/main/perl/bin/ directory in git
- Mo tests: 27/28 passing (99.3%)

- [x] Phase 25: Fix self-referential hash assignment (2026-03-16)
- Root cause: `%h = (new_stuff, %h)` was clearing hash before evaluating `%h`
- Mo uses: `%e = (extends => sub{...}, has => sub{...}, %e)` to merge exports
- The hash was cleared before iterating over the RHS list containing `%h`
- **RuntimeHash.java fix**:
- Materialize entire RHS list into temporary array BEFORE clearing hash
- Similar to how tied hashes are already handled
- This fixed Mo's BUILD feature which depends on the %e merge pattern
- Mo tests: 6/28 failing → 1/28 failing (143/144 subtests pass)

### Current Status

**Test Results (after Phase 23):**
- 62/71 test programs passing (87%)
- ~768/829 subtests passing (93%)
**Test Results (after Phase 25):**
- **Moo**: 62/71 test programs passing (87%), 768/829 subtests passing (93%)
- **Mo**: 27/28 test programs passing (99.3%), 143/144 subtests passing

**Remaining Failures (categorized):**
1. **accessor-weaken tests** (20 failures) - Expected, weak references not supported in Java GC
Expand All @@ -602,6 +625,7 @@ Moo tests run via `jcpan -t Moo`. Recent fixes (Phases 12-13) should improve pas
4. **moo-utils-_subname-Sub-Name.t** (1 failure) - Expected, we have Sub::Util (no fallback to Sub::Name)
5. **no-moo.t** (5 failures) - Namespace cleanup requires weak references
6. **overloaded-coderefs.t** - Expected, B::Deparse not available
7. **Mo t/strict.t** (1 failure) - Error message format differs from Perl

**Expected failures** (not fixable without fundamental changes):
- Weak references: accessor-weaken tests (20), no-moo.t cleanup (5)
Expand All @@ -621,12 +645,16 @@ Moo tests run via `jcpan -t Moo`. Recent fixes (Phases 12-13) should improve pas
### PR Information
- **Branch**: `feature/moo-support` (PR #319 - merged)
- **Branch**: `fix/goto-tailcall-import` (PR #320 - open)
- **Branch**: `fix/mo-bareword-parsing` (PR #322 - open)
- **Key commits**:
- `00c124167` - Fix print { func() } filehandle block parsing and JVM codegen
- `393bedf0f` - Fix quotemeta and Package::SUPER::method resolution
- `7a76739b8` - Fix goto &sub in use/import TAILCALL handling
- `053d91a95` - Add Sub::Util, fix Scalar/List::Util VERSION, add Test::Harness
- `7993ef74d` - Fix version parsing and MM->parse_version for CPAN.pm
- `db434f8d3` - Fix ::identifier bareword parsing and add cpan to sync
- `ff31163f9` - Fix self-referential hash assignment %h = (stuff, %h)
- `a3233cd55` - Improve ::identifier to check sub existence at compile time

## Related Documents

Expand Down
4 changes: 4 additions & 0 deletions dev/import-perl5/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ imports:
- source: perl5/dist/Carp/lib/Carp/Heavy.pm
target: src/main/perl/lib/Carp/Heavy.pm

# CPAN client script - required by jcpan wrapper
- source: perl5/cpan/CPAN/scripts/cpan
target: src/main/perl/bin/cpan

- source: perl5/lib/Benchmark.pm
target: src/main/perl/lib/Benchmark.pm

Expand Down
41 changes: 34 additions & 7 deletions src/main/java/org/perlonjava/frontend/parser/ParsePrimary.java
Original file line number Diff line number Diff line change
Expand Up @@ -269,15 +269,42 @@ static Node parseOperator(Parser parser, LexerToken token, String operator) {
return StringParser.parseRawString(parser, token.text);

case "::":
// Leading :: means main:: (e.g., ::foo is main::foo)
// This allows accessing global variables even when a lexical exists
// Leading :: can mean either:
// 1. Function call: ::foo() -> main::foo()
// 2. Bareword reference: ::foo without parens -> calls main::foo if it exists
// 3. Bareword string: ::foo when no such sub exists -> '::foo'
//
// In Perl, ::foo without parens calls main::foo if the sub exists at compile time.
// If no such sub exists, it becomes a bareword string '::foo'.
LexerToken nextToken2 = peek(parser);
if (nextToken2.type == LexerTokenType.IDENTIFIER) {
// Insert "main" before the :: to create main::identifier
parser.tokens.add(parser.tokenIndex - 1, new LexerToken(LexerTokenType.IDENTIFIER, "main"));
parser.tokenIndex--; // Go back to process "main"
return parseIdentifier(parser, parser.tokenIndex,
new LexerToken(LexerTokenType.IDENTIFIER, "main"), "main");
String identifierName = nextToken2.text;
// Look ahead to see if this is a function call with parens
int lookAhead = parser.tokenIndex + 1;
// Skip whitespace
while (lookAhead < parser.tokens.size() &&
parser.tokens.get(lookAhead).type == LexerTokenType.WHITESPACE) {
lookAhead++;
}
String afterIdentifier = lookAhead < parser.tokens.size() ?
parser.tokens.get(lookAhead).text : "";

// Check if the sub exists at compile time
String fullSubName = "main::" + identifierName;
boolean subExists = GlobalVariable.getGlobalCodeRef(fullSubName).getDefinedBoolean();

if (afterIdentifier.equals("(") || subExists) {
// Function call: ::foo() or ::foo (when sub exists)
// Insert "main" before the :: to create main::identifier
parser.tokens.add(parser.tokenIndex - 1, new LexerToken(LexerTokenType.IDENTIFIER, "main"));
parser.tokenIndex--; // Go back to process "main"
return parseIdentifier(parser, parser.tokenIndex,
new LexerToken(LexerTokenType.IDENTIFIER, "main"), "main");
} else {
// Bareword: ::foo -> '::foo' (no such sub exists)
parser.tokenIndex++; // Consume the identifier
return new StringNode("::" + identifierName, parser.tokenIndex);
}
}
throw new PerlCompilerException(parser.tokenIndex, "syntax error", parser.ctx.errorUtil);

Expand Down
36 changes: 15 additions & 21 deletions src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeHash.java
Original file line number Diff line number Diff line change
Expand Up @@ -189,23 +189,18 @@ public RuntimeScalar addToScalar(RuntimeScalar scalar) {
public RuntimeArray setFromList(RuntimeList value) {
return switch (type) {
case PLAIN_HASH -> {
// Store the original list size for scalar context
int originalSize = 0;
for (RuntimeBase elem : value.elements) {
if (elem instanceof RuntimeArray) {
originalSize += ((RuntimeArray) elem).elements.size();
} else if (elem instanceof RuntimeScalar) {
originalSize++;
} else {
// Count elements by iterating
Iterator<RuntimeScalar> it = elem.iterator();
while (it.hasNext()) {
it.next();
originalSize++;
}
}
// First, fully materialize the right-hand side list BEFORE clearing
// This is critical for self-referential assignments like: %h = (new_stuff, %h)
// We must capture the current hash contents before clearing.
RuntimeArray materializedList = new RuntimeArray();
Iterator<RuntimeScalar> iterator = value.iterator();
while (iterator.hasNext()) {
materializedList.push(new RuntimeScalar(iterator.next()));
}

// Store the original list size for scalar context
int originalSize = materializedList.elements.size();

// Warn about odd elements (Perl does not warn about references in hash assignment)
if (originalSize % 2 != 0) {
WarnDie.warn(
Expand All @@ -216,13 +211,12 @@ public RuntimeArray setFromList(RuntimeList value) {
// Clear existing elements but keep the same Map instance to preserve capacity
this.elements.clear();

// Populate the hash from the provided list
// This reuses the existing StableHashMap and its capacity
Iterator<RuntimeScalar> iter = value.iterator();
while (iter.hasNext()) {
String key = iter.next().toString();
// Populate the hash from the materialized list
iterator = materializedList.iterator();
while (iterator.hasNext()) {
String key = iterator.next().toString();
// Create a new RuntimeScalar to properly handle aliasing and avoid read-only issues
RuntimeScalar val = iter.hasNext() ? new RuntimeScalar(iter.next()) : new RuntimeScalar();
RuntimeScalar val = iterator.hasNext() ? new RuntimeScalar(iterator.next()) : new RuntimeScalar();
this.elements.put(key, val);
}

Expand Down
Loading
Loading