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
233 changes: 131 additions & 102 deletions dev/modules/data_printer.md

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions src/main/java/org/perlonjava/core/Configuration.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,14 @@ public final class Configuration {
* Automatically populated by Gradle/Maven during build.
* DO NOT EDIT MANUALLY - this value is replaced at build time.
*/
public static final String gitCommitId = "03c7e14f2";
public static final String gitCommitId = "dc22ca34e";

/**
* Git commit date of the build (ISO format: YYYY-MM-DD).
* Automatically populated by Gradle/Maven during build.
* DO NOT EDIT MANUALLY - this value is replaced at build time.
*/
public static final String gitCommitDate = "2026-04-03";
public static final String gitCommitDate = "2026-04-04";

// Prevent instantiation
private Configuration() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -486,6 +486,9 @@ static List<String> processPattern(ScalarGlobOperator scalarGlobOperator, String

// Process each pattern
for (String singlePattern : patterns) {
// Expand tilde to home directory
singlePattern = expandTilde(singlePattern);

// Expand braces first
List<String> expandedPatterns = expandBraces(singlePattern);

Expand All @@ -497,6 +500,36 @@ static List<String> processPattern(ScalarGlobOperator scalarGlobOperator, String
return results;
}

/**
* Expands a leading tilde (~) to the user's home directory.
* "~" becomes "/home/user", "~/foo" becomes "/home/user/foo".
*/
private static String expandTilde(String pattern) {
if (pattern.equals("~") || pattern.startsWith("~/")) {
// Check Perl's %ENV{HOME} first (may be overridden by user),
// then fall back to Java system property
String home = null;
try {
org.perlonjava.runtime.runtimetypes.RuntimeHash envHash =
org.perlonjava.runtime.runtimetypes.GlobalVariable.getGlobalHash("main::ENV");
org.perlonjava.runtime.runtimetypes.RuntimeScalar envHome = envHash.get("HOME");
if (envHome != null && envHome.getDefinedBoolean()) {
home = envHome.toString();
}
} catch (Exception e) {
// ignore - fall through to system property
}
if (home == null || home.isEmpty()) {
home = System.getProperty("user.home");
}
if (pattern.equals("~")) {
return home;
}
return home + pattern.substring(1);
}
return pattern;
}

/**
* Parses a pattern string into individual patterns, handling quoted sections.
*
Expand Down
11 changes: 8 additions & 3 deletions src/main/java/org/perlonjava/runtime/operators/TieOperators.java
Original file line number Diff line number Diff line change
Expand Up @@ -120,10 +120,10 @@ public static RuntimeScalar untie(int ctx, RuntimeBase... scalars) {
switch (variable.type) {
case REFERENCE -> {
RuntimeScalar scalar = variable.scalarDeref();
if (scalar.type == TIED_SCALAR) {
if (scalar.type == TIED_SCALAR && scalar.value instanceof TieScalar tieScalar) {
TieScalar.tiedUntie(scalar);
TieScalar.tiedDestroy(scalar);
RuntimeScalar previousValue = ((TieScalar) scalar.value).getPreviousValue();
RuntimeScalar previousValue = tieScalar.getPreviousValue();
scalar.type = previousValue.type;
scalar.value = previousValue.value;
}
Expand Down Expand Up @@ -187,7 +187,12 @@ public static RuntimeScalar tied(int ctx, RuntimeBase... scalars) {
case REFERENCE -> {
RuntimeScalar scalar = variable.scalarDeref();
if (scalar.type == TIED_SCALAR) {
return ((TieScalar) scalar.value).getSelf();
if (scalar.value instanceof TiedVariableBase tvb) {
RuntimeScalar selfObj = tvb.getSelf();
if (selfObj != null) {
return selfObj;
}
}
}
// Handle tied($$glob_ref) where $$glob_ref evaluates to a GLOB wrapped in a reference.
// In Perl 5, tied($$fh) when the glob is tied via tie(*$fh, ...) returns the tied object.
Expand Down
45 changes: 45 additions & 0 deletions src/main/java/org/perlonjava/runtime/perlmodule/Charnames.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package org.perlonjava.runtime.perlmodule;

import com.ibm.icu.lang.UCharacter;
import org.perlonjava.runtime.runtimetypes.RuntimeArray;
import org.perlonjava.runtime.runtimetypes.RuntimeList;
import org.perlonjava.runtime.runtimetypes.RuntimeScalar;

import static org.perlonjava.runtime.runtimetypes.RuntimeScalarCache.scalarUndef;

/**
* Java-side implementation for _charnames module.
* Provides Unicode character name lookup via ICU4J.
*/
public class Charnames extends PerlModuleBase {

public Charnames() {
super("_charnames", false); // Don't set %INC - let the Perl _charnames.pm load normally
}

public static void initialize() {
Charnames charnames = new Charnames();
try {
charnames.registerMethod("_java_viacode", "javaViacode", "$");
} catch (NoSuchMethodException e) {
System.err.println("Warning: Missing _charnames method: " + e.getMessage());
}
}

/**
* Returns the Unicode character name for a given code point.
* Uses ICU4J's UCharacter.getName() which provides full Unicode name data.
*
* @param args Code point as integer
* @param ctx Context
* @return Character name string, or undef if not found
*/
public static RuntimeList javaViacode(RuntimeArray args, int ctx) {
int codePoint = args.getFirst().getInt();
String name = UCharacter.getName(codePoint);
if (name == null || name.isEmpty()) {
return new RuntimeList(scalarUndef);
}
return new RuntimeList(new RuntimeScalar(name));
}
}
40 changes: 18 additions & 22 deletions src/main/java/org/perlonjava/runtime/perlmodule/DBI.java
Original file line number Diff line number Diff line change
Expand Up @@ -116,14 +116,12 @@ public static RuntimeList connect(RuntimeArray args, int ctx) {
String jdbcUrl = args.size() > 1 ? args.get(1).toString() : "";

return executeWithErrorHandling(() -> {
if (args.size() < 4) {
throw new IllegalStateException("Bad number of arguments for DBI->connect");
}

// Extract connection parameters from args
dbh.put("Username", new RuntimeScalar(args.get(2).toString()));
dbh.put("Password", new RuntimeScalar(args.get(3).toString()));
RuntimeScalar attr = args.get(4); // \%attr
// Extract connection parameters from args, defaulting user/pass to empty
String username = args.size() > 2 ? args.get(2).toString() : "";
String password = args.size() > 3 ? args.get(3).toString() : "";
dbh.put("Username", new RuntimeScalar(username));
dbh.put("Password", new RuntimeScalar(password));
RuntimeScalar attr = args.size() > 4 ? args.get(4) : new RuntimeScalar();

// Set dbh attributes
dbh.put("ReadOnly", scalarFalse);
Expand Down Expand Up @@ -157,7 +155,7 @@ public static RuntimeList connect(RuntimeArray args, int ctx) {
dbh.put("Name", new RuntimeScalar(jdbcUrl));

// Create blessed reference for Perl compatibility
RuntimeScalar dbhRef = ReferenceOperators.bless(dbh.createReference(), new RuntimeScalar("DBI"));
RuntimeScalar dbhRef = ReferenceOperators.bless(dbh.createReference(), new RuntimeScalar("DBI::db"));
return dbhRef.getList();
}, dbh, "connect('" + jdbcUrl + "','" + dbh.get("Username") + "',...) failed");
}
Expand Down Expand Up @@ -212,6 +210,7 @@ public static RuntimeList prepare(RuntimeArray args, int ctx) {
// Create statement handle (sth) hash
sth.put("statement", new RuntimeScalar(stmt));
sth.put("sql", new RuntimeScalar(sql));
sth.put("Statement", new RuntimeScalar(sql));
sth.put("Type", new RuntimeScalar("st"));

// Add NUM_OF_FIELDS by getting metadata
Expand All @@ -237,7 +236,7 @@ public static RuntimeList prepare(RuntimeArray args, int ctx) {
sth.put("NUM_OF_PARAMS", new RuntimeScalar(numParams));

// Create blessed reference for statement handle
RuntimeScalar sthRef = ReferenceOperators.bless(sth.createReference(), new RuntimeScalar("DBI"));
RuntimeScalar sthRef = ReferenceOperators.bless(sth.createReference(), new RuntimeScalar("DBI::st"));

dbh.get("sth").set(sthRef);

Expand All @@ -260,18 +259,16 @@ public static RuntimeList last_insert_id(RuntimeArray args, int ctx) {
// Use database-specific SQL to retrieve the last auto-generated ID.
// This is more reliable than getGeneratedKeys() because it works
// regardless of which statement was most recently prepared/executed.
String jdbcUrl = finalDbh.get("Name").toString();
String jdbcUrl = conn.getMetaData().getURL();
String sql;
if (jdbcUrl.contains("sqlite")) {
sql = "SELECT last_insert_rowid()";
} else if (jdbcUrl.contains("mysql") || jdbcUrl.contains("mariadb")) {
sql = "SELECT LAST_INSERT_ID()";
} else if (jdbcUrl.contains("postgresql")) {
sql = "SELECT lastval()";
} else if (jdbcUrl.contains("h2")) {
sql = "SELECT SCOPE_IDENTITY()";
} else {
// Generic fallback: try getGeneratedKeys() on the last statement
// Generic fallback (H2, etc.): use getGeneratedKeys() on the last statement
RuntimeScalar sthRef = finalDbh.get("sth");
if (sthRef != null && RuntimeScalarType.isReference(sthRef)) {
RuntimeHash sth = sthRef.hashDeref();
Expand Down Expand Up @@ -657,7 +654,6 @@ public static RuntimeList disconnect(RuntimeArray args, int ctx) {

return executeWithErrorHandling(() -> {
Connection conn = (Connection) dbh.get("connection").value;
String name = dbh.get("Name").toString();

conn.close();
dbh.put("Active", new RuntimeScalar(false));
Expand Down Expand Up @@ -835,7 +831,7 @@ public static RuntimeList table_info(RuntimeArray args, int ctx) {

// Create statement handle for results
RuntimeHash sth = createMetadataResultSet(dbh, rs);
RuntimeScalar sthRef = ReferenceOperators.bless(sth.createReference(), new RuntimeScalar("DBI"));
RuntimeScalar sthRef = ReferenceOperators.bless(sth.createReference(), new RuntimeScalar("DBI::st"));
return sthRef.getList();
}, dbh, "table_info");
}
Expand All @@ -853,7 +849,7 @@ public static RuntimeList column_info(RuntimeArray args, int ctx) {

// For SQLite, use PRAGMA table_info() to preserve original type case
// (JDBC getColumns() uppercases type names like varchar -> VARCHAR)
String jdbcUrl = dbh.get("Name").toString();
String jdbcUrl = conn.getMetaData().getURL();
if (jdbcUrl.contains("sqlite")) {
return columnInfoViaPragma(dbh, conn, table);
}
Expand All @@ -868,7 +864,7 @@ public static RuntimeList column_info(RuntimeArray args, int ctx) {
ResultSet rs = metaData.getColumns(catalog, schema, table, column);

RuntimeHash sth = createMetadataResultSet(dbh, rs);
RuntimeScalar sthRef = ReferenceOperators.bless(sth.createReference(), new RuntimeScalar("DBI"));
RuntimeScalar sthRef = ReferenceOperators.bless(sth.createReference(), new RuntimeScalar("DBI::st"));
return sthRef.getList();
}, dbh, "column_info");
}
Expand Down Expand Up @@ -956,7 +952,7 @@ private static RuntimeList columnInfoViaPragma(RuntimeHash dbh, Connection conn,
result.put("has_resultset", scalarTrue);
sth.put("execute_result", result.createReference());

RuntimeScalar sthRef = ReferenceOperators.bless(sth.createReference(), new RuntimeScalar("DBI"));
RuntimeScalar sthRef = ReferenceOperators.bless(sth.createReference(), new RuntimeScalar("DBI::st"));
return sthRef.getList();
}

Expand All @@ -978,7 +974,7 @@ public static RuntimeList primary_key_info(RuntimeArray args, int ctx) {
ResultSet rs = metaData.getPrimaryKeys(catalog, schema, table);

RuntimeHash sth = createMetadataResultSet(dbh, rs);
RuntimeScalar sthRef = ReferenceOperators.bless(sth.createReference(), new RuntimeScalar("DBI"));
RuntimeScalar sthRef = ReferenceOperators.bless(sth.createReference(), new RuntimeScalar("DBI::st"));
return sthRef.getList();
}, dbh, "primary_key_info");
}
Expand All @@ -1005,7 +1001,7 @@ public static RuntimeList foreign_key_info(RuntimeArray args, int ctx) {
fkCatalog, fkSchema, fkTable);

RuntimeHash sth = createMetadataResultSet(dbh, rs);
RuntimeScalar sthRef = ReferenceOperators.bless(sth.createReference(), new RuntimeScalar("DBI"));
RuntimeScalar sthRef = ReferenceOperators.bless(sth.createReference(), new RuntimeScalar("DBI::st"));
return sthRef.getList();
}, dbh, "foreign_key_info");
}
Expand All @@ -1019,7 +1015,7 @@ public static RuntimeList type_info(RuntimeArray args, int ctx) {
ResultSet rs = metaData.getTypeInfo();

RuntimeHash sth = createMetadataResultSet(dbh, rs);
RuntimeScalar sthRef = ReferenceOperators.bless(sth.createReference(), new RuntimeScalar("DBI"));
RuntimeScalar sthRef = ReferenceOperators.bless(sth.createReference(), new RuntimeScalar("DBI::st"));
return sthRef.getList();
}, dbh, "type_info");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ protected void registerMethod(String perlMethodName, String javaMethodName, Stri

RuntimeCode code = new RuntimeCode(methodHandle, this, signature);
code.isStatic = true;
code.packageName = moduleName;
code.subName = perlMethodName;

String fullMethodName = NameNormalizer.normalizeVariableName(perlMethodName, moduleName);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,8 @@ private static void addErrno(int code, String msg) {

public ErrnoVariable() {
super();
this.type = RuntimeScalarType.INTEGER;
this.value = 0;
this.type = RuntimeScalarType.DUALVAR;
this.value = new DualVar(new RuntimeScalar(0), new RuntimeScalar(""));
}

/**
Expand All @@ -91,8 +91,8 @@ public ErrnoVariable() {
public RuntimeScalar set(int value) {
this.errno = value;
this.message = ERRNO_MESSAGES.getOrDefault(value, value == 0 ? "" : "Unknown error " + value);
this.type = RuntimeScalarType.INTEGER;
this.value = value;
this.type = RuntimeScalarType.DUALVAR;
this.value = new DualVar(new RuntimeScalar(value), new RuntimeScalar(this.message));
return this;
}

Expand All @@ -107,8 +107,8 @@ public RuntimeScalar set(String value) {
if (value == null || value.isEmpty()) {
this.errno = 0;
this.message = "";
this.type = RuntimeScalarType.INTEGER;
this.value = 0;
this.type = RuntimeScalarType.DUALVAR;
this.value = new DualVar(new RuntimeScalar(0), new RuntimeScalar(""));
return this;
}

Expand All @@ -117,8 +117,8 @@ public RuntimeScalar set(String value) {
if (code != null) {
this.errno = code;
this.message = value;
this.type = RuntimeScalarType.INTEGER;
this.value = code;
this.type = RuntimeScalarType.DUALVAR;
this.value = new DualVar(new RuntimeScalar(code), new RuntimeScalar(value));
return this;
}

Expand All @@ -130,8 +130,8 @@ public RuntimeScalar set(String value) {
// Not a number and not a known message - store as message with errno 0
this.errno = 0;
this.message = value;
this.type = RuntimeScalarType.STRING;
this.value = value;
this.type = RuntimeScalarType.DUALVAR;
this.value = new DualVar(new RuntimeScalar(0), new RuntimeScalar(value));
return this;
}
}
Expand Down Expand Up @@ -199,5 +199,27 @@ public boolean getBoolean() {
public void clear() {
set(0);
}

// Stack to save errno/message during local()
private static final java.util.Stack<int[]> errnoStack = new java.util.Stack<>();
private static final java.util.Stack<String> messageStack = new java.util.Stack<>();

@Override
public void dynamicSaveState() {
errnoStack.push(new int[]{errno});
messageStack.push(message);
super.dynamicSaveState();
}

@Override
public void dynamicRestoreState() {
super.dynamicRestoreState();
if (!errnoStack.isEmpty()) {
errno = errnoStack.pop()[0];
}
if (!messageStack.isEmpty()) {
message = messageStack.pop();
}
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,7 @@ public static void initializeGlobals(CompilerOptions compilerOptions) {
// TimeHiRes.initialize(); // Has XSLoader in Perl file
// Encode.initialize(); // Has XSLoader in Perl file - deferred for Encode::Alias support
UnicodeUCD.initialize(); // No XSLoader in Perl file - needed at startup
Charnames.initialize(); // Java-side charnames::viacode via ICU4J
TermReadLine.initialize(); // No Perl file - needed at startup
TermReadKey.initialize(); // No Perl file - needed at startup
FileTemp.initialize(); // Perl uses eval require - keep for cleanup hooks
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ public static RuntimeScalar makeLocal(String fullName) {
DynamicVariableManager.pushLocalVariable(original);
return original;
}
if (original instanceof ErrnoVariable) {
DynamicVariableManager.pushLocalVariable(original);
return original;
}
if (fullName.endsWith("::1")) {
var regexVar = GlobalVariable.getGlobalVariable(fullName);
DynamicVariableManager.pushLocalVariable(regexVar);
Expand Down
Loading
Loading