diff --git a/dev/design/todo.md b/dev/design/todo.md index 8d30f3bb8..5ebf27691 100644 --- a/dev/design/todo.md +++ b/dev/design/todo.md @@ -3,6 +3,25 @@ ## Warnings to Implement - `"my" variable $x masks earlier declaration in same scope` warning +## Typed Lexical Declarations (`my TYPE $var`) + +Perl supports typed lexical declarations: `my Foo $x`, `my Foo::Bar $x`. +The type annotation is stored in the AST and currently ignored at runtime +(in Perl 5, it is used by the deprecated `fields` pragma for compile-time +hash-key checking via `use fields` / pseudo-hashes). + +### Status +- Simple types work when the package is loaded: `my Foo $x` ✓ +- Qualified types work when the package is loaded: `my Foo::Bar $x` ✓ +- Types accepted without requiring package to be loaded ✓ + (the type is saved as an AST annotation `"varType"` on the declaration node) +- `__PACKAGE__` and `__CLASS__` as type annotations ✓ + +### Future Work +- Validate the type at runtime (emit a warning/error if the class doesn't exist) +- Support `use fields` pragma for compile-time hash-key checking +- Use type annotations for optional JVM type hints or optimization + ## More Difficult, and Low Impact - `goto()` to jump to a label in the call stack - Thread diff --git a/src/main/java/org/perlonjava/backend/jvm/EmitVariable.java b/src/main/java/org/perlonjava/backend/jvm/EmitVariable.java index 0ecce00c2..140d6fb8f 100644 --- a/src/main/java/org/perlonjava/backend/jvm/EmitVariable.java +++ b/src/main/java/org/perlonjava/backend/jvm/EmitVariable.java @@ -1213,6 +1213,16 @@ private static void emitStateInitialization(EmitterVisitor emitterVisitor, Binar static void handleMyOperator(EmitterVisitor emitterVisitor, OperatorNode node) { EmitterContext ctx = emitterVisitor.ctx; + // Emit runtime "No such class" check for typed declarations (my TYPE $var) + String varType = node.getAnnotation("varType") != null ? (String) node.getAnnotation("varType") : null; + if (varType != null) { + ctx.mv.visitLdcInsn(varType); + ctx.mv.visitMethodInsn(Opcodes.INVOKESTATIC, + "org/perlonjava/runtime/runtimetypes/GlobalVariable", + "checkClassExists", + "(Ljava/lang/String;)V", false); + } + String operator = node.operator; if (CompilerOptions.DEBUG_ENABLED) ctx.logDebug("handleMyOperator: operator=" + operator + ", operand type=" + (node.operand != null ? node.operand.getClass().getSimpleName() : "null") + ", contextType=" + ctx.contextType); if (node.operand instanceof ListNode listNode) { // my ($a, $b) our ($a, $b) diff --git a/src/main/java/org/perlonjava/core/Configuration.java b/src/main/java/org/perlonjava/core/Configuration.java index 11e2bf154..1359fd2ac 100644 --- a/src/main/java/org/perlonjava/core/Configuration.java +++ b/src/main/java/org/perlonjava/core/Configuration.java @@ -33,7 +33,7 @@ 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 = "f006e8c7b"; + public static final String gitCommitId = "582f96865"; /** * Git commit date of the build (ISO format: YYYY-MM-DD). @@ -48,7 +48,7 @@ public final class Configuration { * Parsed by App::perlbrew and other tools via: perl -V | grep "Compiled at" * DO NOT EDIT MANUALLY - this value is replaced at build time. */ - public static final String buildTimestamp = "Apr 13 2026 11:44:38"; + public static final String buildTimestamp = "Apr 13 2026 13:52:26"; // Prevent instantiation private Configuration() { diff --git a/src/main/java/org/perlonjava/frontend/parser/OperatorParser.java b/src/main/java/org/perlonjava/frontend/parser/OperatorParser.java index 1167eaa03..8e1e0afb9 100644 --- a/src/main/java/org/perlonjava/frontend/parser/OperatorParser.java +++ b/src/main/java/org/perlonjava/frontend/parser/OperatorParser.java @@ -365,12 +365,22 @@ static OperatorNode parseVariableDeclaration(Parser parser, String operator, int TokenUtils.consume(parser); // consume __PACKAGE__/__CLASS__ varType = parser.ctx.symbolTable.getCurrentPackage(); } else { - // If a package name follows, then it is a type declaration + // If a package name follows, then it is a type declaration. + // In Perl, `my Foo::Bar $x` is valid when Foo::Bar is loaded. + // We accept the type name at parse time when unambiguously followed + // by a sigil ($, @, %, \) or opening paren, and defer the "No such class" + // check to runtime — matching Perl's behavior while handling the JVM + // architectural difference (entire file compiled before execution). int currentIndex2 = parser.tokenIndex; String packageName = IdentifierParser.parseSubroutineIdentifier(parser); - boolean packageExists = GlobalVariable.isPackageLoaded(packageName); - // System.out.println("maybe type: " + packageName + " " + packageExists); - if (packageExists) { + LexerToken afterType = peek(parser); + boolean followedBySigil = "$".equals(afterType.text) || "@".equals(afterType.text) + || "%".equals(afterType.text) || "\\".equals(afterType.text) + || "(".equals(afterType.text); + if (followedBySigil) { + // Unambiguously a type annotation (followed by a variable sigil or paren list) + varType = packageName; + } else if (GlobalVariable.isPackageLoaded(packageName)) { varType = packageName; } else { // Backtrack @@ -508,6 +518,9 @@ static OperatorNode parseVariableDeclaration(Parser parser, String operator, int if (isDeclaredReference) { decl.setAnnotation("isDeclaredReference", true); } + if (varType != null) { + decl.setAnnotation("varType", varType); + } // Initialize a list to store any attributes the declaration might have. List attributes = new ArrayList<>(); diff --git a/src/main/java/org/perlonjava/frontend/parser/SubroutineParser.java b/src/main/java/org/perlonjava/frontend/parser/SubroutineParser.java index cd4c45fd5..ed3d4f3cc 100644 --- a/src/main/java/org/perlonjava/frontend/parser/SubroutineParser.java +++ b/src/main/java/org/perlonjava/frontend/parser/SubroutineParser.java @@ -357,7 +357,18 @@ static Node parseSubroutineCall(Parser parser, boolean isMethod) { || nextTok.text.equals(":")); if (!terminator && !infixOp - && nextTok.type != LexerTokenType.IDENTIFIER + && (nextTok.type != LexerTokenType.IDENTIFIER + || (subName.contains("::") + && !nextTok.text.equals("or") + && !nextTok.text.equals("and") + && !nextTok.text.equals("not") + && !nextTok.text.equals("if") + && !nextTok.text.equals("unless") + && !nextTok.text.equals("while") + && !nextTok.text.equals("until") + && !nextTok.text.equals("for") + && !nextTok.text.equals("foreach") + && !nextTok.text.equals("when"))) && !nextTok.text.equals("->") && !nextTok.text.equals("=>")) { // Check if this looks like indirect object syntax: method $object, args diff --git a/src/main/java/org/perlonjava/runtime/nativ/ffm/FFMPosixInterface.java b/src/main/java/org/perlonjava/runtime/nativ/ffm/FFMPosixInterface.java index 81eeb2da2..e1c49d2d5 100644 --- a/src/main/java/org/perlonjava/runtime/nativ/ffm/FFMPosixInterface.java +++ b/src/main/java/org/perlonjava/runtime/nativ/ffm/FFMPosixInterface.java @@ -269,6 +269,65 @@ public interface FFMPosixInterface { */ int nativeDup(int fd); + // ==================== Low-level File Descriptor Functions ==================== + + /** + * Create a pipe. + * @param fds Array of at least 2 ints: fds[0] = read end, fds[1] = write end + * @return 0 on success, -1 on error (check errno) + */ + int pipe(int[] fds); + + /** + * Duplicate a file descriptor. + * @param fd File descriptor to duplicate + * @return New file descriptor, or -1 on error (check errno) + */ + int dup(int fd); + + /** + * Open a file. + * @param path File path + * @param flags Open flags (O_RDONLY, O_WRONLY, O_RDWR, O_CREAT, etc.) + * @param mode Permission mode (used with O_CREAT) + * @return File descriptor, or -1 on error (check errno) + */ + int open(String path, int flags, int mode); + + /** + * Close a file descriptor. + * @param fd File descriptor to close + * @return 0 on success, -1 on error (check errno) + */ + int close(int fd); + + /** + * Read from a file descriptor. + * @param fd File descriptor + * @param buf Buffer to read into + * @param count Maximum number of bytes to read + * @return Number of bytes read, 0 at EOF, -1 on error (check errno) + */ + long read(int fd, byte[] buf, long count); + + /** + * Write to a file descriptor. + * @param fd File descriptor + * @param buf Buffer to write from + * @param count Number of bytes to write + * @return Number of bytes written, -1 on error (check errno) + */ + long write(int fd, byte[] buf, long count); + + /** + * Reposition read/write file offset. + * @param fd File descriptor + * @param offset Offset in bytes + * @param whence SEEK_SET (0), SEEK_CUR (1), or SEEK_END (2) + * @return Resulting offset from beginning of file, -1 on error + */ + long lseek(int fd, long offset, int whence); + // ==================== File Control Functions ==================== /** diff --git a/src/main/java/org/perlonjava/runtime/nativ/ffm/FFMPosixLinux.java b/src/main/java/org/perlonjava/runtime/nativ/ffm/FFMPosixLinux.java index 91a732da8..f31f67e88 100644 --- a/src/main/java/org/perlonjava/runtime/nativ/ffm/FFMPosixLinux.java +++ b/src/main/java/org/perlonjava/runtime/nativ/ffm/FFMPosixLinux.java @@ -65,16 +65,20 @@ public class FFMPosixLinux implements FFMPosixInterface { private static MethodHandle ptsnameHandle; private static MethodHandle setsidHandle; private static MethodHandle ttynameHandle; + // Method handles for low-level FD operations + private static MethodHandle pipeHandle; private static MethodHandle openHandle; private static MethodHandle closeHandle; private static MethodHandle readHandle; private static MethodHandle writeHandle; private static MethodHandle dupHandle; + private static MethodHandle lseekHandle; private static MethodHandle fcntlHandle; private static MethodHandle ioctlPtrHandle; private static MethodHandle ioctlIntHandle; private static MethodHandle tcgetattrHandle; private static MethodHandle tcsetattrHandle; + private static MethodHandle nativeOpenHandle; // 2-arg open for PTY operations // Platform-specific constants for PTY operations public static final int O_RDWR; @@ -85,7 +89,6 @@ public class FFMPosixLinux implements FFMPosixInterface { public static final long TIOCNOTTY; public static final int F_DUPFD = 0; // Same on both platforms public static final int TERMIOS_SIZE; - static { if (IS_MACOS) { O_RDWR = 0x0002; @@ -292,38 +295,60 @@ private static synchronized void ensureInitialized() { FunctionDescriptor.of(ValueLayout.ADDRESS, ValueLayout.JAVA_INT) ); - // open: int open(const char *path, int flags) - openHandle = linker.downcallHandle( + // nativeOpen: int open(const char *path, int flags) - 2-arg for PTY operations + nativeOpenHandle = linker.downcallHandle( stdlib.find("open").orElseThrow(), FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.JAVA_INT), captureErrno ); - // close: int close(int fd) + // Low-level FD operations with errno capture + // int pipe(int pipefd[2]) + pipeHandle = linker.downcallHandle( + stdlib.find("pipe").orElseThrow(), + FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS), + captureErrno + ); + + // int dup(int oldfd) + dupHandle = linker.downcallHandle( + stdlib.find("dup").orElseThrow(), + FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.JAVA_INT), + captureErrno + ); + + // int open(const char *pathname, int flags, mode_t mode) + openHandle = linker.downcallHandle( + stdlib.find("open").orElseThrow(), + FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.JAVA_INT, ValueLayout.JAVA_INT), + captureErrno + ); + + // int close(int fd) closeHandle = linker.downcallHandle( stdlib.find("close").orElseThrow(), FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.JAVA_INT), captureErrno ); - // read: ssize_t read(int fd, void *buf, size_t count) + // ssize_t read(int fd, void *buf, size_t count) readHandle = linker.downcallHandle( stdlib.find("read").orElseThrow(), FunctionDescriptor.of(ValueLayout.JAVA_LONG, ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.JAVA_LONG), captureErrno ); - // write: ssize_t write(int fd, const void *buf, size_t count) + // ssize_t write(int fd, const void *buf, size_t count) writeHandle = linker.downcallHandle( stdlib.find("write").orElseThrow(), FunctionDescriptor.of(ValueLayout.JAVA_LONG, ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.JAVA_LONG), captureErrno ); - // dup: int dup(int fd) - dupHandle = linker.downcallHandle( - stdlib.find("dup").orElseThrow(), - FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.JAVA_INT), + // off_t lseek(int fd, off_t offset, int whence) + lseekHandle = linker.downcallHandle( + stdlib.find("lseek").orElseThrow(), + FunctionDescriptor.of(ValueLayout.JAVA_LONG, ValueLayout.JAVA_INT, ValueLayout.JAVA_LONG, ValueLayout.JAVA_INT), captureErrno ); @@ -799,6 +824,139 @@ public int isatty(int fd) { } } + // ==================== Low-level FD Functions ==================== + + @Override + public int pipe(int[] fds) { + ensureInitialized(); + try (Arena arena = Arena.ofConfined()) { + MemorySegment pipeBuf = arena.allocate(ValueLayout.JAVA_INT, 2); + MemorySegment capturedState = arena.allocate(Linker.Option.captureStateLayout()); + int result = (int) pipeHandle.invokeExact(capturedState, pipeBuf); + if (result == -1) { + int err = capturedState.get(ValueLayout.JAVA_INT, errnoOffset); + setErrno(err); + return -1; + } + fds[0] = pipeBuf.getAtIndex(ValueLayout.JAVA_INT, 0); + fds[1] = pipeBuf.getAtIndex(ValueLayout.JAVA_INT, 1); + return 0; + } catch (Throwable e) { + setErrno(24); // EMFILE + return -1; + } + } + + @Override + public int dup(int fd) { + ensureInitialized(); + try (Arena arena = Arena.ofConfined()) { + MemorySegment capturedState = arena.allocate(Linker.Option.captureStateLayout()); + int result = (int) dupHandle.invokeExact(capturedState, fd); + if (result == -1) { + int err = capturedState.get(ValueLayout.JAVA_INT, errnoOffset); + setErrno(err); + } + return result; + } catch (Throwable e) { + setErrno(9); // EBADF + return -1; + } + } + + @Override + public int open(String path, int flags, int mode) { + ensureInitialized(); + try (Arena arena = Arena.ofConfined()) { + MemorySegment pathSegment = arena.allocateFrom(path); + MemorySegment capturedState = arena.allocate(Linker.Option.captureStateLayout()); + int result = (int) openHandle.invokeExact(capturedState, pathSegment, flags, mode); + if (result == -1) { + int err = capturedState.get(ValueLayout.JAVA_INT, errnoOffset); + setErrno(err); + } + return result; + } catch (Throwable e) { + setErrno(5); // EIO + return -1; + } + } + + @Override + public int close(int fd) { + ensureInitialized(); + try (Arena arena = Arena.ofConfined()) { + MemorySegment capturedState = arena.allocate(Linker.Option.captureStateLayout()); + int result = (int) closeHandle.invokeExact(capturedState, fd); + if (result == -1) { + int err = capturedState.get(ValueLayout.JAVA_INT, errnoOffset); + setErrno(err); + } + return result; + } catch (Throwable e) { + setErrno(9); // EBADF + return -1; + } + } + + @Override + public long read(int fd, byte[] buf, long count) { + ensureInitialized(); + try (Arena arena = Arena.ofConfined()) { + MemorySegment nativeBuf = arena.allocate(count); + MemorySegment capturedState = arena.allocate(Linker.Option.captureStateLayout()); + long result = (long) readHandle.invokeExact(capturedState, fd, nativeBuf, count); + if (result == -1) { + int err = capturedState.get(ValueLayout.JAVA_INT, errnoOffset); + setErrno(err); + return -1; + } + // Copy data from native buffer to Java array + if (result > 0) { + MemorySegment.copy(nativeBuf, ValueLayout.JAVA_BYTE, 0, buf, 0, (int) result); + } + return result; + } catch (Throwable e) { + setErrno(5); // EIO + return -1; + } + } + + @Override + public long write(int fd, byte[] buf, long count) { + ensureInitialized(); + try (Arena arena = Arena.ofConfined()) { + MemorySegment nativeBuf = arena.allocateFrom(ValueLayout.JAVA_BYTE, buf); + MemorySegment capturedState = arena.allocate(Linker.Option.captureStateLayout()); + long result = (long) writeHandle.invokeExact(capturedState, fd, nativeBuf, count); + if (result == -1) { + int err = capturedState.get(ValueLayout.JAVA_INT, errnoOffset); + setErrno(err); + } + return result; + } catch (Throwable e) { + setErrno(5); // EIO + return -1; + } + } + + @Override + public long lseek(int fd, long offset, int whence) { + ensureInitialized(); + try (Arena arena = Arena.ofConfined()) { + MemorySegment capturedState = arena.allocate(Linker.Option.captureStateLayout()); + long result = (long) lseekHandle.invokeExact(capturedState, fd, offset, whence); + if (result == -1) { + int err = capturedState.get(ValueLayout.JAVA_INT, errnoOffset); + setErrno(err); + } + return result; + } catch (Throwable e) { + setErrno(9); // EBADF + return -1; + } + } + // ==================== File Control Functions ==================== @Override @@ -926,7 +1084,7 @@ public int nativeOpen(String path, int flags) { try (Arena arena = Arena.ofConfined()) { MemorySegment pathSegment = arena.allocateFrom(path); MemorySegment capturedState = arena.allocate(Linker.Option.captureStateLayout()); - int result = (int) openHandle.invokeExact(capturedState, pathSegment, flags); + int result = (int) nativeOpenHandle.invokeExact(capturedState, pathSegment, flags); if (result == -1) { int err = capturedState.get(ValueLayout.JAVA_INT, errnoOffset); setErrno(err); diff --git a/src/main/java/org/perlonjava/runtime/nativ/ffm/FFMPosixWindows.java b/src/main/java/org/perlonjava/runtime/nativ/ffm/FFMPosixWindows.java index 5decbbb5a..87759bad8 100644 --- a/src/main/java/org/perlonjava/runtime/nativ/ffm/FFMPosixWindows.java +++ b/src/main/java/org/perlonjava/runtime/nativ/ffm/FFMPosixWindows.java @@ -1,6 +1,13 @@ package org.perlonjava.runtime.nativ.ffm; import java.io.Console; +import java.lang.foreign.Arena; +import java.lang.foreign.FunctionDescriptor; +import java.lang.foreign.Linker; +import java.lang.foreign.MemorySegment; +import java.lang.foreign.SymbolLookup; +import java.lang.foreign.ValueLayout; +import java.lang.invoke.MethodHandle; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.attribute.BasicFileAttributes; @@ -45,6 +52,74 @@ public class FFMPosixWindows implements FFMPosixInterface { Math.abs(computerName.hashCode()) % ID_RANGE : DEFAULT_GID; } + // Lazy-initialized FFM components for MSVCRT calls + private static volatile boolean fdOpsInitialized = false; + private static MethodHandle winPipeHandle; + private static MethodHandle winDupHandle; + private static MethodHandle winOpenHandle; + private static MethodHandle winCloseHandle; + private static MethodHandle winReadHandle; + private static MethodHandle winWriteHandle; + private static MethodHandle winLseekHandle; + + /** + * Initialize FFM bindings for MSVCRT low-level FD operations on Windows. + */ + private static synchronized void ensureFdOpsInitialized() { + if (fdOpsInitialized) return; + + try { + Linker linker = Linker.nativeLinker(); + SymbolLookup ucrt = SymbolLookup.libraryLookup("ucrtbase", Arena.global()); + + // int _pipe(int *pfds, unsigned int psize, int textmode) + winPipeHandle = linker.downcallHandle( + ucrt.find("_pipe").orElseThrow(), + FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.JAVA_INT, ValueLayout.JAVA_INT) + ); + + // int _dup(int fd) + winDupHandle = linker.downcallHandle( + ucrt.find("_dup").orElseThrow(), + FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.JAVA_INT) + ); + + // int _open(const char *filename, int oflag, int pmode) + winOpenHandle = linker.downcallHandle( + ucrt.find("_open").orElseThrow(), + FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.JAVA_INT, ValueLayout.JAVA_INT) + ); + + // int _close(int fd) + winCloseHandle = linker.downcallHandle( + ucrt.find("_close").orElseThrow(), + FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.JAVA_INT) + ); + + // int _read(int fd, void *buffer, unsigned int count) + winReadHandle = linker.downcallHandle( + ucrt.find("_read").orElseThrow(), + FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.JAVA_INT) + ); + + // int _write(int fd, const void *buffer, unsigned int count) + winWriteHandle = linker.downcallHandle( + ucrt.find("_write").orElseThrow(), + FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.JAVA_INT) + ); + + // long _lseek(int fd, long offset, int origin) + winLseekHandle = linker.downcallHandle( + ucrt.find("_lseek").orElseThrow(), + FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.JAVA_INT, ValueLayout.JAVA_INT, ValueLayout.JAVA_INT) + ); + + fdOpsInitialized = true; + } catch (Throwable e) { + throw new RuntimeException("Failed to initialize Windows FFM MSVCRT bindings", e); + } + } + // ==================== Process Functions ==================== @Override @@ -374,6 +449,128 @@ public int tcsetattr(int fd, int optionalActions, byte[] termios) { throw new UnsupportedOperationException("tcsetattr is not supported on Windows"); } + // ==================== Low-level FD Functions ==================== + + @Override + public int pipe(int[] fds) { + ensureFdOpsInitialized(); + try (Arena arena = Arena.ofConfined()) { + MemorySegment pipeBuf = arena.allocate(ValueLayout.JAVA_INT, 2); + // _pipe(pfds, 4096, _O_BINARY) - 0x8000 is _O_BINARY on Windows + int result = (int) winPipeHandle.invoke(pipeBuf, 4096, 0x8000); + if (result == -1) { + setErrno(24); // EMFILE + return -1; + } + fds[0] = pipeBuf.getAtIndex(ValueLayout.JAVA_INT, 0); + fds[1] = pipeBuf.getAtIndex(ValueLayout.JAVA_INT, 1); + return 0; + } catch (Throwable e) { + setErrno(24); // EMFILE + return -1; + } + } + + @Override + public int dup(int fd) { + ensureFdOpsInitialized(); + try { + int result = (int) winDupHandle.invoke(fd); + if (result == -1) { + setErrno(9); // EBADF + } + return result; + } catch (Throwable e) { + setErrno(9); // EBADF + return -1; + } + } + + @Override + public int open(String path, int flags, int mode) { + ensureFdOpsInitialized(); + try (Arena arena = Arena.ofConfined()) { + MemorySegment pathSegment = arena.allocateFrom(path); + // Add _O_BINARY flag (0x8000) to prevent text-mode translation + int result = (int) winOpenHandle.invoke(pathSegment, flags | 0x8000, mode); + if (result == -1) { + setErrno(2); // ENOENT + } + return result; + } catch (Throwable e) { + setErrno(5); // EIO + return -1; + } + } + + @Override + public int close(int fd) { + ensureFdOpsInitialized(); + try { + int result = (int) winCloseHandle.invoke(fd); + if (result == -1) { + setErrno(9); // EBADF + } + return result; + } catch (Throwable e) { + setErrno(9); // EBADF + return -1; + } + } + + @Override + public long read(int fd, byte[] buf, long count) { + ensureFdOpsInitialized(); + try (Arena arena = Arena.ofConfined()) { + int intCount = (int) Math.min(count, Integer.MAX_VALUE); + MemorySegment nativeBuf = arena.allocate(intCount); + int result = (int) winReadHandle.invoke(fd, nativeBuf, intCount); + if (result == -1) { + setErrno(5); // EIO + return -1; + } + if (result > 0) { + MemorySegment.copy(nativeBuf, ValueLayout.JAVA_BYTE, 0, buf, 0, result); + } + return result; + } catch (Throwable e) { + setErrno(5); // EIO + return -1; + } + } + + @Override + public long write(int fd, byte[] buf, long count) { + ensureFdOpsInitialized(); + try (Arena arena = Arena.ofConfined()) { + int intCount = (int) Math.min(count, Integer.MAX_VALUE); + MemorySegment nativeBuf = arena.allocateFrom(ValueLayout.JAVA_BYTE, buf); + int result = (int) winWriteHandle.invoke(fd, nativeBuf, intCount); + if (result == -1) { + setErrno(5); // EIO + } + return result; + } catch (Throwable e) { + setErrno(5); // EIO + return -1; + } + } + + @Override + public long lseek(int fd, long offset, int whence) { + ensureFdOpsInitialized(); + try { + int result = (int) winLseekHandle.invoke(fd, (int) offset, whence); + if (result == -1) { + setErrno(9); // EBADF + } + return result; + } catch (Throwable e) { + setErrno(9); // EBADF + return -1; + } + } + // ==================== File Control Functions ==================== @Override diff --git a/src/main/java/org/perlonjava/runtime/perlmodule/POSIX.java b/src/main/java/org/perlonjava/runtime/perlmodule/POSIX.java index 7ac6d074e..2b0b913a4 100644 --- a/src/main/java/org/perlonjava/runtime/perlmodule/POSIX.java +++ b/src/main/java/org/perlonjava/runtime/perlmodule/POSIX.java @@ -43,8 +43,16 @@ public static void initialize() { module.registerMethod("_strerror", "strerror", null); module.registerMethod("_access", "access", null); module.registerMethod("_dup2", "dup2", null); - module.registerMethod("_dup", "dup", null); + + // Low-level FD operations + module.registerMethod("_pipe", "posix_pipe", null); + module.registerMethod("_dup", "posix_dup", null); + module.registerMethod("_open", "posix_open", null); module.registerMethod("_close", "posix_close", null); + module.registerMethod("_read", "posix_read", null); + module.registerMethod("_write", "posix_write", null); + module.registerMethod("_lseek", "posix_lseek", null); + module.registerMethod("_fcntl", "posix_fcntl", null); module.registerMethod("_isatty", "posix_isatty", null); module.registerMethod("_setsid", "posix_setsid", null); module.registerMethod("_ttyname", "posix_ttyname", null); @@ -100,6 +108,15 @@ public static void initialize() { module.registerMethod("_const_VSUSP", "const_VSUSP", null); module.registerMethod("_const_VTIME", "const_VTIME", null); + // Fcntl constants + module.registerMethod("_const_F_DUPFD", "const_F_DUPFD", null); + module.registerMethod("_const_F_GETFD", "const_F_GETFD", null); + module.registerMethod("_const_F_SETFD", "const_F_SETFD", null); + module.registerMethod("_const_F_GETFL", "const_F_GETFL", null); + module.registerMethod("_const_F_SETFL", "const_F_SETFL", null); + module.registerMethod("_const_FD_CLOEXEC", "const_FD_CLOEXEC", null); + module.registerMethod("_const_O_NONBLOCK", "const_O_NONBLOCK", null); + // Access constants module.registerMethod("_const_F_OK", "const_F_OK", null); module.registerMethod("_const_R_OK", "const_R_OK", null); @@ -934,6 +951,194 @@ public static RuntimeList access(RuntimeArray args, int ctx) { return new RuntimeScalar("0 but true").getList(); } + // ==================== Low-level FD Operations ==================== + + /** + * POSIX::pipe() - create a pipe. + * Returns (read_fd, write_fd) on success, empty list on failure. + */ + public static RuntimeList posix_pipe(RuntimeArray args, int ctx) { + try { + var posix = FFMPosix.get(); + int[] fds = new int[2]; + int result = posix.pipe(fds); + if (result == -1) { + GlobalVariable.getGlobalVariable("main::!").set(posix.strerror(posix.errno())); + return new RuntimeList(); + } + RuntimeList list = new RuntimeList(); + list.add(new RuntimeScalar(fds[0])); + list.add(new RuntimeScalar(fds[1])); + return list; + } catch (Exception e) { + GlobalVariable.getGlobalVariable("main::!").set(e.getMessage()); + return new RuntimeList(); + } + } + + /** + * POSIX::dup(fd) - duplicate a file descriptor. + * Returns new fd on success, undef on failure. + */ + public static RuntimeList posix_dup(RuntimeArray args, int ctx) { + if (args.isEmpty()) { + GlobalVariable.getGlobalVariable("main::!").set("Bad file descriptor"); + return RuntimeScalarCache.scalarUndef.getList(); + } + int fd = args.get(0).getInt(); + try { + var posix = FFMPosix.get(); + int result = posix.dup(fd); + if (result == -1) { + GlobalVariable.getGlobalVariable("main::!").set(posix.strerror(posix.errno())); + return RuntimeScalarCache.scalarUndef.getList(); + } + return new RuntimeScalar(result).getList(); + } catch (Exception e) { + GlobalVariable.getGlobalVariable("main::!").set(e.getMessage()); + return RuntimeScalarCache.scalarUndef.getList(); + } + } + + /** + * POSIX::open(path, flags [, mode]) - open a file using native OS call. + * Returns fd on success, undef on failure. + */ + public static RuntimeList posix_open(RuntimeArray args, int ctx) { + if (args.isEmpty()) { + GlobalVariable.getGlobalVariable("main::!").set("Bad file descriptor"); + return RuntimeScalarCache.scalarUndef.getList(); + } + String path = args.get(0).toString(); + int flags = args.size() > 1 ? args.get(1).getInt() : 0; // O_RDONLY + int mode = args.size() > 2 ? args.get(2).getInt() : 0666; + try { + var posix = FFMPosix.get(); + int result = posix.open(path, flags, mode); + if (result == -1) { + GlobalVariable.getGlobalVariable("main::!").set(posix.strerror(posix.errno())); + return RuntimeScalarCache.scalarUndef.getList(); + } + return new RuntimeScalar(result == 0 ? "0 but true" : (Object) result).getList(); + } catch (Exception e) { + GlobalVariable.getGlobalVariable("main::!").set(e.getMessage()); + return RuntimeScalarCache.scalarUndef.getList(); + } + } + + /** + * POSIX::read(fd, buf, nbytes) - read from a file descriptor. + * Reads into the second argument (buffer scalar). + * Returns number of bytes read, 0 at EOF, undef on error. + */ + public static RuntimeList posix_read(RuntimeArray args, int ctx) { + if (args.size() < 3) { + GlobalVariable.getGlobalVariable("main::!").set("Bad file descriptor"); + return RuntimeScalarCache.scalarUndef.getList(); + } + int fd = args.get(0).getInt(); + int nbytes = args.get(2).getInt(); + try { + var posix = FFMPosix.get(); + byte[] buf = new byte[nbytes]; + long result = posix.read(fd, buf, nbytes); + if (result == -1) { + GlobalVariable.getGlobalVariable("main::!").set(posix.strerror(posix.errno())); + return RuntimeScalarCache.scalarUndef.getList(); + } + // Set the buffer (args[1]) to the data read + if (result > 0) { + args.get(1).set(new String(buf, 0, (int) result)); + } else { + args.get(1).set(""); + } + return new RuntimeScalar(result == 0 ? "0 but true" : (Object) result).getList(); + } catch (Exception e) { + GlobalVariable.getGlobalVariable("main::!").set(e.getMessage()); + return RuntimeScalarCache.scalarUndef.getList(); + } + } + + /** + * POSIX::write(fd, buf, nbytes) - write to a file descriptor. + * Returns number of bytes written, undef on error. + */ + public static RuntimeList posix_write(RuntimeArray args, int ctx) { + if (args.size() < 3) { + GlobalVariable.getGlobalVariable("main::!").set("Bad file descriptor"); + return RuntimeScalarCache.scalarUndef.getList(); + } + int fd = args.get(0).getInt(); + String data = args.get(1).toString(); + int nbytes = args.get(2).getInt(); + try { + var posix = FFMPosix.get(); + byte[] buf = data.getBytes(); + int writeLen = Math.min(nbytes, buf.length); + long result = posix.write(fd, buf, writeLen); + if (result == -1) { + GlobalVariable.getGlobalVariable("main::!").set(posix.strerror(posix.errno())); + return RuntimeScalarCache.scalarUndef.getList(); + } + return new RuntimeScalar(result == 0 ? "0 but true" : (Object) result).getList(); + } catch (Exception e) { + GlobalVariable.getGlobalVariable("main::!").set(e.getMessage()); + return RuntimeScalarCache.scalarUndef.getList(); + } + } + + /** + * POSIX::lseek(fd, offset, whence) - seek on a file descriptor. + * Returns resulting offset, undef on error. + */ + public static RuntimeList posix_lseek(RuntimeArray args, int ctx) { + if (args.size() < 3) { + GlobalVariable.getGlobalVariable("main::!").set("Bad file descriptor"); + return RuntimeScalarCache.scalarUndef.getList(); + } + int fd = args.get(0).getInt(); + long offset = args.get(1).getLong(); + int whence = args.get(2).getInt(); + try { + var posix = FFMPosix.get(); + long result = posix.lseek(fd, offset, whence); + if (result == -1) { + GlobalVariable.getGlobalVariable("main::!").set(posix.strerror(posix.errno())); + return RuntimeScalarCache.scalarUndef.getList(); + } + return new RuntimeScalar(result == 0 ? "0 but true" : (Object) result).getList(); + } catch (Exception e) { + GlobalVariable.getGlobalVariable("main::!").set(e.getMessage()); + return RuntimeScalarCache.scalarUndef.getList(); + } + } + + /** + * POSIX::fcntl(fd, cmd, arg) - file control. + * Returns result, undef on error. + */ + public static RuntimeList posix_fcntl(RuntimeArray args, int ctx) { + if (args.size() < 2) { + GlobalVariable.getGlobalVariable("main::!").set("Bad file descriptor"); + return RuntimeScalarCache.scalarUndef.getList(); + } + int fd = args.get(0).getInt(); + int cmd = args.get(1).getInt(); + int arg = args.size() > 2 ? args.get(2).getInt() : 0; + try { + var posix = FFMPosix.get(); + int result = posix.fcntl(fd, cmd, arg); + if (result == -1) { + GlobalVariable.getGlobalVariable("main::!").set(posix.strerror(posix.errno())); + return RuntimeScalarCache.scalarUndef.getList(); + } + return new RuntimeScalar(result == 0 ? "0 but true" : (Object) result).getList(); + } catch (Exception e) { + GlobalVariable.getGlobalVariable("main::!").set(e.getMessage()); + return RuntimeScalarCache.scalarUndef.getList(); + } + } + /** * POSIX dup2(oldfd, newfd) - duplicate a file descriptor onto a specific fd. * Used by IO::Socket::IP to replace a socket's underlying fd. @@ -1056,6 +1261,17 @@ public static RuntimeList const_SEEK_END(RuntimeArray args, int ctx) { return new RuntimeScalar(2).getList(); } + // Fcntl constants + public static RuntimeList const_F_DUPFD(RuntimeArray a, int c) { return new RuntimeScalar(0).getList(); } + public static RuntimeList const_F_GETFD(RuntimeArray a, int c) { return new RuntimeScalar(1).getList(); } + public static RuntimeList const_F_SETFD(RuntimeArray a, int c) { return new RuntimeScalar(2).getList(); } + public static RuntimeList const_F_GETFL(RuntimeArray a, int c) { return new RuntimeScalar(3).getList(); } + public static RuntimeList const_F_SETFL(RuntimeArray a, int c) { return new RuntimeScalar(4).getList(); } + public static RuntimeList const_FD_CLOEXEC(RuntimeArray a, int c) { return new RuntimeScalar(1).getList(); } + public static RuntimeList const_O_NONBLOCK(RuntimeArray a, int c) { + return new RuntimeScalar(IS_WINDOWS ? 0 : 2048).getList(); // 04000 on Unix + } + // POSIX wait status macros // In Unix, wait() returns a status where: // - If exited normally: bits 8-15 = exit code, bits 0-7 = 0 diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/GlobalVariable.java b/src/main/java/org/perlonjava/runtime/runtimetypes/GlobalVariable.java index 6fdcb25ab..eeb3f5ed1 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/GlobalVariable.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/GlobalVariable.java @@ -649,6 +649,19 @@ public static boolean isPackageLoaded(String className) { return exists; } + /** + * Runtime check for typed lexical declarations (my TYPE $var). + * Throws a compile-time-like error matching Perl's "No such class TYPE" + * if the package is not loaded at the point of execution. + * + * @param className The type annotation class name + */ + public static void checkClassExists(String className) { + if (!isPackageLoaded(className)) { + throw new RuntimeException("No such class " + className); + } + } + /** * Resolves a fully-qualified variable name through stash hash redirections. *

diff --git a/src/main/perl/lib/POSIX.pm b/src/main/perl/lib/POSIX.pm index bbcfe588a..bbaf9605c 100644 --- a/src/main/perl/lib/POSIX.pm +++ b/src/main/perl/lib/POSIX.pm @@ -10,6 +10,7 @@ our $VERSION = '2.21'; use strict; use warnings; +use Carp; use Exporter (); use XSLoader; @@ -307,6 +308,9 @@ sub rmdir { POSIX::_rmdir(@_) } sub getcwd { POSIX::_getcwd() } sub chdir { POSIX::_chdir(@_) } +# File control +sub fcntl { POSIX::_fcntl(@_) } + # Terminal functions sub isatty { my $fd = ref($_[0]) ? fileno($_[0]) : $_[0]; @@ -511,6 +515,8 @@ for my $const (qw( F_OK R_OK W_OK X_OK + F_DUPFD F_GETFD F_SETFD F_GETFL F_SETFL FD_CLOEXEC + SIGHUP SIGINT SIGQUIT SIGILL SIGTRAP SIGABRT SIGBUS SIGFPE SIGKILL SIGUSR1 SIGSEGV SIGUSR2 SIGPIPE SIGALRM SIGTERM SIGCHLD SIGCONT SIGSTOP SIGTSTP