From bdd280dcc6448111f0a631ffbb491917b4d3dc11 Mon Sep 17 00:00:00 2001 From: Pierre Donat-Bouillud Date: Thu, 22 Feb 2024 17:14:35 -0500 Subject: [PATCH 01/31] RDSWriter --- src/main/java/org/prlprg/rds/RDSWriter.java | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 src/main/java/org/prlprg/rds/RDSWriter.java diff --git a/src/main/java/org/prlprg/rds/RDSWriter.java b/src/main/java/org/prlprg/rds/RDSWriter.java new file mode 100644 index 000000000..961ce6169 --- /dev/null +++ b/src/main/java/org/prlprg/rds/RDSWriter.java @@ -0,0 +1,6 @@ +package org.prlprg.rds; + +import java.io.*; +import org.prlprg.sexp.*; + +public class RDSWriter implements Closeable {} From 50f12a65a08b857e27fcab2693710f6e9fcd8cd4 Mon Sep 17 00:00:00 2001 From: Pierre Donat-Bouillud Date: Thu, 22 Feb 2024 17:16:55 -0500 Subject: [PATCH 02/31] Add information in the README: - requires java 21 at least - requires to have npm installed --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a7227ed67..0ffde9c76 100644 --- a/README.md +++ b/README.md @@ -19,10 +19,12 @@ import javax.annotation.ParametersAreNonnullByDefault; In IntelliJ you can simply copy `package-info.json` from any package into the new one and it will create this. +The project requires at least Java 21. + ## Commands - Run `make setup` to install Git Hooks. The commit hook formats, the pre-push hook runs tests and static analyses. - Build with `make` or `mvn package` - Test (no static analyses) with `make test` or `mvn test` - Test and static anaylses with `make check` or `mvn verify` -- Format with `make format` or `mvn spotless:apply` +- Format with `make format` or `mvn spotless:apply`. This requires to have `npm` installed. From 271b2a22f83e1a44c5bee7d1999ed4dcab360f32 Mon Sep 17 00:00:00 2001 From: Pierre Donat-Bouillud Date: Thu, 22 Feb 2024 17:48:47 -0500 Subject: [PATCH 03/31] RDSOutputStream --- .../java/org/prlprg/rds/RDSOutputStream.java | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 src/main/java/org/prlprg/rds/RDSOutputStream.java diff --git a/src/main/java/org/prlprg/rds/RDSOutputStream.java b/src/main/java/org/prlprg/rds/RDSOutputStream.java new file mode 100644 index 000000000..6e9201586 --- /dev/null +++ b/src/main/java/org/prlprg/rds/RDSOutputStream.java @@ -0,0 +1,49 @@ +package org.prlprg.rds; + +import java.io.Closeable; +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.OutputStream; + +public class RDSOutputStream implements Closeable { + private final DataOutputStream out; + + RDSOutputStream(OutputStream out) { + this.out = new DataOutputStream(out); + } + + @Override + public void close() throws IOException { + out.close(); + } + + public void writeByte(int v) throws IOException { + out.writeByte(v); + } + + public void writeInt(int v) throws IOException { + out.writeInt(v); + } + + public void writeDouble(double v) throws IOException { + out.writeDouble(v); + } + + public void writeString(String s) throws IOException { + // one byte per character. + // Fixme: supports the charset (when a character is more than 1 byte!) + out.writeBytes(s); + } + + public void writeInts(int[] v) throws IOException { + for (int e : v) { + out.writeInt(e); + } + } + + public void writeDoubles(double[] v) throws IOException { + for (double e : v) { + out.writeDouble(e); + } + } +} From 5d4135c19a3f459491fb94a2ca94ab6ff2b8539a Mon Sep 17 00:00:00 2001 From: Pierre Donat-Bouillud Date: Tue, 5 Mar 2024 17:52:32 +0100 Subject: [PATCH 04/31] Minimal compiling RDSWriter --- src/main/java/org/prlprg/rds/RDSWriter.java | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/prlprg/rds/RDSWriter.java b/src/main/java/org/prlprg/rds/RDSWriter.java index 961ce6169..47926872b 100644 --- a/src/main/java/org/prlprg/rds/RDSWriter.java +++ b/src/main/java/org/prlprg/rds/RDSWriter.java @@ -1,6 +1,21 @@ package org.prlprg.rds; import java.io.*; + +import org.prlprg.RSession; import org.prlprg.sexp.*; -public class RDSWriter implements Closeable {} +public class RDSWriter implements Closeable { + private final RSession session; + private final RDSOutputStream out; + + private RDSWriter(RSession session, OutputStream out) { + this.session = session; + this.out = new RDSOutputStream(out); + } + + @Override + public void close() throws IOException { + out.close(); + } +} From 0abd2f0482e307f04335be86766a0bf7bb07d02d Mon Sep 17 00:00:00 2001 From: Pierre Donat-Bouillud Date: Thu, 7 Mar 2024 16:58:15 +0100 Subject: [PATCH 05/31] Names of symbol should be non null. --- src/main/java/org/prlprg/sexp/RegSymSXP.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/prlprg/sexp/RegSymSXP.java b/src/main/java/org/prlprg/sexp/RegSymSXP.java index a29f532c0..04f1250a0 100644 --- a/src/main/java/org/prlprg/sexp/RegSymSXP.java +++ b/src/main/java/org/prlprg/sexp/RegSymSXP.java @@ -2,6 +2,7 @@ import com.google.common.base.Objects; import com.google.common.collect.ImmutableList; +import edu.umd.cs.findbugs.annotations.NonNull; import java.util.Optional; /** Symbol which isn't "unbound value" or "missing arg" */ @@ -9,7 +10,7 @@ public final class RegSymSXP implements SymSXP, StrOrRegSymSXP { private static final ImmutableList LITERAL_NAMES = ImmutableList.of("TRUE", "FALSE", "NULL", "NA", "Inf", "NaN"); - private final String name; + private final @NonNull String name; private final boolean isEscaped; RegSymSXP(String name) { From a99082a4669fc52539330e7d9022a9949d370209 Mon Sep 17 00:00:00 2001 From: Pierre Donat-Bouillud Date: Mon, 11 Mar 2024 16:22:52 +0100 Subject: [PATCH 06/31] Support most common sexp types for RDS writing --- .githooks/pre-push.sh | 2 - .idea/.gitignore | 2 + .settings/org.eclipse.jdt.core.prefs | 12 +- src/main/java/org/prlprg/RVersion.java | 9 + .../java/org/prlprg/primitive/Logical.java | 5 + .../java/org/prlprg/rds/RDSOutputStream.java | 2 +- src/main/java/org/prlprg/rds/RDSWriter.java | 277 +++++++++++++++++- .../java/org/prlprg/sexp/NamespaceEnvSXP.java | 4 + src/main/java/org/prlprg/sexp/SEXP.java | 4 + 9 files changed, 303 insertions(+), 14 deletions(-) delete mode 100755 .githooks/pre-push.sh diff --git a/.githooks/pre-push.sh b/.githooks/pre-push.sh deleted file mode 100755 index 09b2a4d0b..000000000 --- a/.githooks/pre-push.sh +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/sh -mvn verify diff --git a/.idea/.gitignore b/.idea/.gitignore index 13566b81b..a9d7db9c0 100644 --- a/.idea/.gitignore +++ b/.idea/.gitignore @@ -6,3 +6,5 @@ # Datasource local storage ignored files /dataSources/ /dataSources.local.xml +# GitHub Copilot persisted chat sessions +/copilot/chatSessions diff --git a/.settings/org.eclipse.jdt.core.prefs b/.settings/org.eclipse.jdt.core.prefs index 64161896b..305face14 100644 --- a/.settings/org.eclipse.jdt.core.prefs +++ b/.settings/org.eclipse.jdt.core.prefs @@ -1,11 +1,19 @@ eclipse.preferences.version=1 -org.eclipse.jdt.core.compiler.annotation.missingNonNullByDefaultAnnotation=disabled +org.eclipse.jdt.core.compiler.annotation.missingNonNullByDefaultAnnotation=ignore org.eclipse.jdt.core.compiler.annotation.nonnull=javax.annotation.Nonnull -org.eclipse.jdt.core.compiler.annotation.nonnullbydefault=org.eclipse.jdt.annotation.NonNullByDefault +org.eclipse.jdt.core.compiler.annotation.nonnullbydefault=javax.annotation.ParametersAreNonnullByDefault org.eclipse.jdt.core.compiler.annotation.nullable=javax.annotation.Nullable org.eclipse.jdt.core.compiler.annotation.nullanalysis=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=21 +org.eclipse.jdt.core.compiler.compliance=21 +org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled +org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning org.eclipse.jdt.core.compiler.problem.nullAnnotationInferenceConflict=warning org.eclipse.jdt.core.compiler.problem.nullReference=warning org.eclipse.jdt.core.compiler.problem.nullSpecViolation=warning org.eclipse.jdt.core.compiler.problem.potentialNullReference=warning +org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=ignore org.eclipse.jdt.core.compiler.problem.syntacticNullAnalysisForFields=enabled +org.eclipse.jdt.core.compiler.processAnnotations=disabled +org.eclipse.jdt.core.compiler.release=disabled +org.eclipse.jdt.core.compiler.source=21 diff --git a/src/main/java/org/prlprg/RVersion.java b/src/main/java/org/prlprg/RVersion.java index d21ff66f1..c307ef3b8 100644 --- a/src/main/java/org/prlprg/RVersion.java +++ b/src/main/java/org/prlprg/RVersion.java @@ -50,6 +50,15 @@ public static RVersion parse(String textual) { this(major, minor, patch, null); } + /** + * Encode the version as an integer. It is used for the RDS serialization for instance. + * + * @return + */ + public int encode() { + return patch + 256 * minor + 65536 * major; + } + @Override public String toString() { return major + "." + minor + "." + patch + (suffix == null ? "" : "-" + suffix); diff --git a/src/main/java/org/prlprg/primitive/Logical.java b/src/main/java/org/prlprg/primitive/Logical.java index 3344b6032..8f4aaa986 100644 --- a/src/main/java/org/prlprg/primitive/Logical.java +++ b/src/main/java/org/prlprg/primitive/Logical.java @@ -22,6 +22,11 @@ public static Logical valueOf(int i) { }; } + /** Convert to GNU-R representation. */ + public int toInt() { + return i; + } + Logical(int i) { this.i = i; } diff --git a/src/main/java/org/prlprg/rds/RDSOutputStream.java b/src/main/java/org/prlprg/rds/RDSOutputStream.java index 6e9201586..993ec375c 100644 --- a/src/main/java/org/prlprg/rds/RDSOutputStream.java +++ b/src/main/java/org/prlprg/rds/RDSOutputStream.java @@ -17,7 +17,7 @@ public void close() throws IOException { out.close(); } - public void writeByte(int v) throws IOException { + public void writeByte(byte v) throws IOException { out.writeByte(v); } diff --git a/src/main/java/org/prlprg/rds/RDSWriter.java b/src/main/java/org/prlprg/rds/RDSWriter.java index 47926872b..805ec0376 100644 --- a/src/main/java/org/prlprg/rds/RDSWriter.java +++ b/src/main/java/org/prlprg/rds/RDSWriter.java @@ -1,21 +1,280 @@ package org.prlprg.rds; import java.io.*; - +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.stream.StreamSupport; import org.prlprg.RSession; +import org.prlprg.RVersion; +import org.prlprg.primitive.Logical; import org.prlprg.sexp.*; public class RDSWriter implements Closeable { - private final RSession session; - private final RDSOutputStream out; + private final RSession session; + private final RDSOutputStream out; + private final List refTable = new ArrayList<>(128); + + private RDSWriter(RSession session, OutputStream out) { + this.session = session; + this.out = new RDSOutputStream(out); + } - private RDSWriter(RSession session, OutputStream out) { - this.session = session; - this.out = new RDSOutputStream(out); + public static void writeStream(RSession session, OutputStream output, SEXP sexp) + throws IOException { + try (var writer = new RDSWriter(session, output)) { + writer.write(sexp); } + } + + public void writeheader(SEXP sexp) throws IOException { + // Could also be "X" (XDR) and "A" (ASCII) but we only support binary + out.writeByte((byte) 'B'); + out.writeByte((byte) '\n'); + + // Always write version 3 of the encoding + out.writeInt(3); + + // Version of R for the writer + out.writeInt(RVersion.LATEST_AWARE.encode()); + + // Minimal version of R required to read back + out.writeInt((new RVersion(3, 5, 0, null)).encode()); + + // Charset. Always UTF-8. + var nativeEncoding = StandardCharsets.UTF_8.name(); + out.writeInt(nativeEncoding.length()); + out.writeString(nativeEncoding); + } + + public void write(SEXP sexp) throws IOException { + writeheader(sexp); + throw new UnsupportedOperationException("not implemented yet"); + } + + // See + // https://github.com/wch/r-source/blob/65892cc124ac20a44950e6e432f9860b1d6e9bf4/src/main/serialize.c#L1021 + public void writeItem(SEXP s) throws IOException { + // ALTREP: TODO + + // Persisted through the ref table? TODO + + // Save Special hooks: direct return and exit the function after a special hook + switch (s) { + case NilSXP _nil -> { + out.writeInt(RDSItemType.Special.NILVALUE_SXP.i()); + return; + } + case EmptyEnvSXP _empty -> { + out.writeInt(RDSItemType.Special.EMPTYENV_SXP.i()); + return; + } + + case BaseEnvSXP _base -> { + out.writeInt(RDSItemType.Special.BASEENV_SXP.i()); + return; + } - @Override - public void close() throws IOException { - out.close(); + case GlobalEnvSXP _global -> { + out.writeInt(RDSItemType.Special.GLOBALENV_SXP.i()); + return; + } + + case SpecialSymSXP sexp when sexp == SEXPs.UNBOUND_VALUE -> { + out.writeInt(RDSItemType.Special.UNBOUNDVALUE_SXP.i()); + return; + } + + case SpecialSymSXP sexp when sexp == SEXPs.MISSING_ARG -> { + out.writeInt(RDSItemType.Special.MISSINGARG_SXP.i()); + return; + } + + // BaseNamespace not supported + + default -> { + // continue + } } + + // Already in the ref table? TODO + + // Symbols + if (s instanceof RegSymSXP sym) { + refTable.add(s); + out.writeByte((byte) SEXPType.SYM.ordinal()); + // write the name (cannot be NA) + out.writeInt(sym.name().length()); + out.writeString(sym.name()); + } else if (s.type() == SEXPType.ENV) { + refTable.add(s); + + // Package + // TODO: add PackageEnv to the EnvSXP interface + StrSXP name = (StrSXP) Objects.requireNonNull(s.attributes()).get("name"); + assert name != null; + if (name.get(0).startsWith("package:")) { + out.writeInt(RDSItemType.Special.PACKAGESXP.ordinal()); + writeItem(name); + } // Namespace + else if (s instanceof NamespaceEnvSXP) { + out.writeInt(RDSItemType.Special.NAMESPACESXP.ordinal()); + // TODO: build STRSXP with name and version + throw new UnsupportedOperationException("not implemented yet"); + } else { // Any other env + out.writeInt(SEXPType.ENV.ordinal()); + out.writeInt(0); // FIXME: should be 1 if locked, 0 if not locked + // Enclosure + // Frame + // Hashtab + // Attributes + } + } else { + int hastag = + switch (s.type()) { + case LIST, LANG, PROM -> { + yield 0; + } // we assume no tag + case CLO -> { + yield 1; + } + default -> { + yield 0; + } + }; + // Flags + int hasattr = s.type() != SEXPType.CHAR && s.attributes() != null ? 1 : 0; + // levels in the sxpinfo gp field + int flags = packFlags(s.type().ordinal(), 0, s.isObject() ? 1 : 0, hasattr, hastag); + out.writeInt(flags); + + switch (s) { + case ListSXP l -> writeDottedPairObjects(l, hasattr == 1); + case LangSXP l -> writeDottedPairObjects(l, hasattr == 1); + case PromSXP p -> writeDottedPairObjects(p, hasattr == 1); + case CloSXP c -> writeDottedPairObjects(c, hasattr == 1); + // External pointer + // weakreference + // special + // builtin + case StrSXP str -> writeStrSxp(str, hasattr == 1); + case VectorSXP vec -> writeVector(vec, hasattr == 1); + case BCodeSXP bcode -> writeBCode(bcode); + + default -> throw new UnsupportedOperationException("Unsupported SEXP type: " + s.type()); + } + } + + throw new UnsupportedOperationException("not implemented yet"); + } + + // Handles LISTSXP (pairlist, LANGSXP, PROMSXP and DOTSXP + private void writeDottedPairObjects(SEXP s, boolean hasattr) throws IOException { + // first, attributes + if (hasattr) { + writeAttributes(s.attributes()); + } + + // tag (except for closures + // TODO + + // car + switch (s) { + case LangSXP l -> { + for (var i : l.asList()) { + writeItem(i); + } + } + case PromSXP p -> { + // a promise has the value, expression and environment, in this order + writeItem(p.getVal()); + writeItem(p.getExpr()); + writeItem(p.getEnv()); + } + case CloSXP c -> { + // a closure has the environment, formals, and then body + writeItem(c.env()); + writeItem(c.formals()); + writeItem(c.body()); + } + default -> throw new RuntimeException("Unreachable"); + } + } + + private int packFlags(int type, int levs, int isobj, int hasattr, int hastag) { + throw new UnsupportedOperationException("not implemented yet"); + } + + private void writeAttributes(Attributes attrs) throws IOException { + throw new UnsupportedOperationException("not implemented yet"); + } + + private void writeChars(String s) throws IOException { + // Never NA because we assume so + // We only consider scalar Na strings + out.writeInt(s.length()); + out.writeString(s); + } + + private void writeStrSxp(StrSXP s, boolean hasattr) throws IOException { + out.writeInt(s.size()); + for (var str : s) { + writeChars(str); + } + + if (hasattr) { + writeAttributes(Objects.requireNonNull(s.attributes())); + } + } + + private void writeVector(VectorSXP s, boolean hasattr) throws IOException { + out.writeInt(s.size()); + + switch (s) { + case VecSXP vec -> { + for (var val : vec) { + writeItem(val); + } + } + + case ExprSXP expr -> { + for (var val : expr) { + writeItem(val); + } + } + case IntSXP intVec -> { + var vec = StreamSupport.stream(intVec.spliterator(), false).mapToInt(i -> i).toArray(); + out.writeInts(vec); + } + case LglSXP lglSXP -> { + var vec = + StreamSupport.stream(lglSXP.spliterator(), false).mapToInt(Logical::toInt).toArray(); + out.writeInts(vec); + } + case RealSXP realSXP -> { + var vec = StreamSupport.stream(realSXP.spliterator(), false).mapToDouble(d -> d).toArray(); + out.writeDoubles(vec); + } + + case ComplexSXP _complexSXP -> { + throw new UnsupportedOperationException("not implemented yet"); + } + + default -> throw new RuntimeException("Unreachable: implemented in another branch."); + } + + if (hasattr) { + writeAttributes(Objects.requireNonNull(s.attributes())); + } + } + + private void writeBCode(BCodeSXP s) throws IOException { + throw new UnsupportedOperationException("not implemented yet"); + } + + @Override + public void close() throws IOException { + out.close(); + } } diff --git a/src/main/java/org/prlprg/sexp/NamespaceEnvSXP.java b/src/main/java/org/prlprg/sexp/NamespaceEnvSXP.java index 6045f37b1..774747e4d 100644 --- a/src/main/java/org/prlprg/sexp/NamespaceEnvSXP.java +++ b/src/main/java/org/prlprg/sexp/NamespaceEnvSXP.java @@ -34,6 +34,10 @@ public String getVersion() { return version; } + public String getName() { + return name; + } + @Override public String toString() { // TODO: add some link to the R session? diff --git a/src/main/java/org/prlprg/sexp/SEXP.java b/src/main/java/org/prlprg/sexp/SEXP.java index 8cb874178..f52a2a50b 100644 --- a/src/main/java/org/prlprg/sexp/SEXP.java +++ b/src/main/java/org/prlprg/sexp/SEXP.java @@ -43,4 +43,8 @@ default SEXP withClass(String name) { var attrs = Objects.requireNonNull(attributes()).including("class", SEXPs.string(name)); return withAttributes(attrs); } + + default boolean isObject() { + return attributes() != null && Objects.requireNonNull(attributes()).containsKey("class"); + } } From a5e6af9b5062350e0d502f7d5acf4df9d8f677e1 Mon Sep 17 00:00:00 2001 From: Pierre Donat-Bouillud Date: Tue, 12 Mar 2024 15:45:56 +0100 Subject: [PATCH 07/31] Write read test for integer vector --- src/main/java/org/prlprg/rds/RDSWriter.java | 74 +++++++++++-------- .../java/org/prlprg/rds/RDSWriterTest.java | 41 ++++++++++ 2 files changed, 84 insertions(+), 31 deletions(-) create mode 100644 src/test/java/org/prlprg/rds/RDSWriterTest.java diff --git a/src/main/java/org/prlprg/rds/RDSWriter.java b/src/main/java/org/prlprg/rds/RDSWriter.java index 805ec0376..6a2fcee5e 100644 --- a/src/main/java/org/prlprg/rds/RDSWriter.java +++ b/src/main/java/org/prlprg/rds/RDSWriter.java @@ -16,6 +16,10 @@ public class RDSWriter implements Closeable { private final RDSOutputStream out; private final List refTable = new ArrayList<>(128); + public static final int IS_OBJECT_BIT_MASK = 1 << 8; + public static final int HAS_ATTR_BIT_MASK = 1 << 9; + public static final int HAS_TAG_BIT_MASK = 1 << 10; + private RDSWriter(RSession session, OutputStream out) { this.session = session; this.out = new RDSOutputStream(out); @@ -29,8 +33,9 @@ public static void writeStream(RSession session, OutputStream output, SEXP sexp) } public void writeheader(SEXP sexp) throws IOException { - // Could also be "X" (XDR) and "A" (ASCII) but we only support binary - out.writeByte((byte) 'B'); + // Could also be "B" (binary) and "A" (ASCII) but we only support XDR. + // XDR just means big endian and DataInputStream/DataOutputStream from Java use BigEndian + out.writeByte((byte) 'X'); out.writeByte((byte) '\n'); // Always write version 3 of the encoding @@ -50,7 +55,7 @@ public void writeheader(SEXP sexp) throws IOException { public void write(SEXP sexp) throws IOException { writeheader(sexp); - throw new UnsupportedOperationException("not implemented yet"); + writeItem(sexp); } // See @@ -103,7 +108,7 @@ public void writeItem(SEXP s) throws IOException { // Symbols if (s instanceof RegSymSXP sym) { refTable.add(s); - out.writeByte((byte) SEXPType.SYM.ordinal()); + out.writeByte((byte) SEXPType.SYM.i); // write the name (cannot be NA) out.writeInt(sym.name().length()); out.writeString(sym.name()); @@ -115,15 +120,15 @@ public void writeItem(SEXP s) throws IOException { StrSXP name = (StrSXP) Objects.requireNonNull(s.attributes()).get("name"); assert name != null; if (name.get(0).startsWith("package:")) { - out.writeInt(RDSItemType.Special.PACKAGESXP.ordinal()); + out.writeInt(RDSItemType.Special.PACKAGESXP.i()); writeItem(name); } // Namespace else if (s instanceof NamespaceEnvSXP) { - out.writeInt(RDSItemType.Special.NAMESPACESXP.ordinal()); + out.writeInt(RDSItemType.Special.NAMESPACESXP.i()); // TODO: build STRSXP with name and version throw new UnsupportedOperationException("not implemented yet"); } else { // Any other env - out.writeInt(SEXPType.ENV.ordinal()); + out.writeInt(SEXPType.ENV.i); out.writeInt(0); // FIXME: should be 1 if locked, 0 if not locked // Enclosure // Frame @@ -131,49 +136,44 @@ else if (s instanceof NamespaceEnvSXP) { // Attributes } } else { - int hastag = + boolean hastag = switch (s.type()) { - case LIST, LANG, PROM -> { - yield 0; - } // we assume no tag - case CLO -> { - yield 1; - } - default -> { - yield 0; - } + case LIST, LANG, PROM -> false; // we assume no tag + case CLO -> true; + default -> false; }; // Flags - int hasattr = s.type() != SEXPType.CHAR && s.attributes() != null ? 1 : 0; + boolean hasattr = + s.type() != SEXPType.CHAR + && s.attributes() != null + && !Objects.requireNonNull(s.attributes()).isEmpty(); // levels in the sxpinfo gp field - int flags = packFlags(s.type().ordinal(), 0, s.isObject() ? 1 : 0, hasattr, hastag); + int flags = packFlags(s.type().i, 0, s.isObject(), hasattr, hastag); out.writeInt(flags); switch (s) { - case ListSXP l -> writeDottedPairObjects(l, hasattr == 1); - case LangSXP l -> writeDottedPairObjects(l, hasattr == 1); - case PromSXP p -> writeDottedPairObjects(p, hasattr == 1); - case CloSXP c -> writeDottedPairObjects(c, hasattr == 1); + case ListSXP l -> writeDottedPairObjects(l, hasattr); + case LangSXP l -> writeDottedPairObjects(l, hasattr); + case PromSXP p -> writeDottedPairObjects(p, hasattr); + case CloSXP c -> writeDottedPairObjects(c, hasattr); // External pointer // weakreference // special // builtin - case StrSXP str -> writeStrSxp(str, hasattr == 1); - case VectorSXP vec -> writeVector(vec, hasattr == 1); + case StrSXP str -> writeStrSxp(str, hasattr); + case VectorSXP vec -> writeVector(vec, hasattr); case BCodeSXP bcode -> writeBCode(bcode); default -> throw new UnsupportedOperationException("Unsupported SEXP type: " + s.type()); } } - - throw new UnsupportedOperationException("not implemented yet"); } // Handles LISTSXP (pairlist, LANGSXP, PROMSXP and DOTSXP private void writeDottedPairObjects(SEXP s, boolean hasattr) throws IOException { // first, attributes if (hasattr) { - writeAttributes(s.attributes()); + writeAttributes(Objects.requireNonNull(s.attributes())); } // tag (except for closures @@ -202,8 +202,20 @@ private void writeDottedPairObjects(SEXP s, boolean hasattr) throws IOException } } - private int packFlags(int type, int levs, int isobj, int hasattr, int hastag) { - throw new UnsupportedOperationException("not implemented yet"); + private int packFlags(int type, int levs, boolean isobj, boolean hasattr, boolean hastag) { + int val = type; + val |= levs << 12; + if (isobj) { + val |= IS_OBJECT_BIT_MASK; + } + if (hasattr) { + val |= HAS_ATTR_BIT_MASK; + } + if (hastag) { + val |= HAS_TAG_BIT_MASK; + } + + return val; } private void writeAttributes(Attributes attrs) throws IOException { @@ -228,7 +240,7 @@ private void writeStrSxp(StrSXP s, boolean hasattr) throws IOException { } } - private void writeVector(VectorSXP s, boolean hasattr) throws IOException { + private void writeVector(VectorSXP s, boolean hasattr) throws IOException { out.writeInt(s.size()); switch (s) { diff --git a/src/test/java/org/prlprg/rds/RDSWriterTest.java b/src/test/java/org/prlprg/rds/RDSWriterTest.java new file mode 100644 index 000000000..11507748d --- /dev/null +++ b/src/test/java/org/prlprg/rds/RDSWriterTest.java @@ -0,0 +1,41 @@ +package org.prlprg.rds; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import org.junit.jupiter.api.Test; +import org.prlprg.RSession; +import org.prlprg.rsession.TestRSession; +import org.prlprg.sexp.*; +import org.prlprg.util.GNUR; +import org.prlprg.util.Tests; + +public class RDSWriterTest implements Tests { + private final RSession rsession = new TestRSession(); + + private final GNUR R = new GNUR(rsession); + + @Test + public void testInts() throws Exception { + var ints = SEXPs.integer(5, 4, 3, 2, 1); + var output = new ByteArrayOutputStream(); + + RDSWriter.writeStream(rsession, output, ints); + + var input = new ByteArrayInputStream(output.toByteArray()); + var sexp = RDSReader.readStream(rsession, input); + + if (sexp instanceof IntSXP read_ints) { + assertEquals(5, read_ints.size()); + assertEquals(5, read_ints.get(0)); + assertEquals(4, read_ints.get(1)); + assertEquals(3, read_ints.get(2)); + assertEquals(2, read_ints.get(3)); + assertEquals(1, read_ints.get(4)); + } else { + fail("Expected IntSXP"); + } + } +} From eb4174c38b9c36256aaf1b308f0e4345a2be08bc Mon Sep 17 00:00:00 2001 From: Pierre Donat-Bouillud Date: Tue, 12 Mar 2024 17:28:00 +0100 Subject: [PATCH 08/31] Test for Logical + associated builder --- src/main/java/org/prlprg/rds/RDSWriter.java | 3 ++- src/main/java/org/prlprg/sexp/SEXPs.java | 8 +++++++ .../java/org/prlprg/rds/RDSWriterTest.java | 21 +++++++++++++++++++ 3 files changed, 31 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/prlprg/rds/RDSWriter.java b/src/main/java/org/prlprg/rds/RDSWriter.java index 6a2fcee5e..af76c5caf 100644 --- a/src/main/java/org/prlprg/rds/RDSWriter.java +++ b/src/main/java/org/prlprg/rds/RDSWriter.java @@ -176,7 +176,7 @@ private void writeDottedPairObjects(SEXP s, boolean hasattr) throws IOException writeAttributes(Objects.requireNonNull(s.attributes())); } - // tag (except for closures + // tag (except for closures) // TODO // car @@ -225,6 +225,7 @@ private void writeAttributes(Attributes attrs) throws IOException { private void writeChars(String s) throws IOException { // Never NA because we assume so // We only consider scalar Na strings + // TODO: write flags also here out.writeInt(s.length()); out.writeString(s); } diff --git a/src/main/java/org/prlprg/sexp/SEXPs.java b/src/main/java/org/prlprg/sexp/SEXPs.java index fb45fac91..d4f2cd584 100644 --- a/src/main/java/org/prlprg/sexp/SEXPs.java +++ b/src/main/java/org/prlprg/sexp/SEXPs.java @@ -68,6 +68,14 @@ public static IntSXP integer(int first, int... rest) { return integer(ImmutableIntArray.of(first, rest)); } + public static LglSXP logical(Logical first, Logical... rest) { + return logical( + ImmutableList.builderWithExpectedSize(rest.length + 1) + .add(first) + .add(rest) + .build()); + } + public static RealSXP real(double first, double... rest) { return real(ImmutableDoubleArray.of(first, rest)); } diff --git a/src/test/java/org/prlprg/rds/RDSWriterTest.java b/src/test/java/org/prlprg/rds/RDSWriterTest.java index 11507748d..f2d180a83 100644 --- a/src/test/java/org/prlprg/rds/RDSWriterTest.java +++ b/src/test/java/org/prlprg/rds/RDSWriterTest.java @@ -7,6 +7,7 @@ import java.io.ByteArrayOutputStream; import org.junit.jupiter.api.Test; import org.prlprg.RSession; +import org.prlprg.primitive.Logical; import org.prlprg.rsession.TestRSession; import org.prlprg.sexp.*; import org.prlprg.util.GNUR; @@ -38,4 +39,24 @@ public void testInts() throws Exception { fail("Expected IntSXP"); } } + + @Test + public void testLgls() throws Exception { + var lgls = SEXPs.logical(Logical.TRUE, Logical.FALSE, Logical.NA); + var output = new ByteArrayOutputStream(); + + RDSWriter.writeStream(rsession, output, lgls); + + var input = new ByteArrayInputStream(output.toByteArray()); + var sexp = RDSReader.readStream(rsession, input); + + if (sexp instanceof LglSXP read_lgls) { + assertEquals(3, read_lgls.size()); + assertEquals(Logical.TRUE, read_lgls.get(0)); + assertEquals(Logical.FALSE, read_lgls.get(1)); + assertEquals(Logical.NA, read_lgls.get(2)); + } else { + fail("Expected LglSXP"); + } + } } From c634080492e96d4db5da85811e6ac1f1730a8509 Mon Sep 17 00:00:00 2001 From: Pierre Donat-Bouillud Date: Tue, 12 Mar 2024 17:38:10 +0100 Subject: [PATCH 09/31] Tests for reals and null --- src/main/java/org/prlprg/rds/RDSWriter.java | 1 - .../java/org/prlprg/rds/RDSWriterTest.java | 37 +++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/prlprg/rds/RDSWriter.java b/src/main/java/org/prlprg/rds/RDSWriter.java index af76c5caf..5316cf089 100644 --- a/src/main/java/org/prlprg/rds/RDSWriter.java +++ b/src/main/java/org/prlprg/rds/RDSWriter.java @@ -250,7 +250,6 @@ private void writeVector(VectorSXP s, boolean hasattr) throws IOException writeItem(val); } } - case ExprSXP expr -> { for (var val : expr) { writeItem(val); diff --git a/src/test/java/org/prlprg/rds/RDSWriterTest.java b/src/test/java/org/prlprg/rds/RDSWriterTest.java index f2d180a83..127714b13 100644 --- a/src/test/java/org/prlprg/rds/RDSWriterTest.java +++ b/src/test/java/org/prlprg/rds/RDSWriterTest.java @@ -1,5 +1,6 @@ package org.prlprg.rds; +import static java.lang.Double.NaN; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.fail; @@ -7,6 +8,7 @@ import java.io.ByteArrayOutputStream; import org.junit.jupiter.api.Test; import org.prlprg.RSession; +import org.prlprg.primitive.Constants; import org.prlprg.primitive.Logical; import org.prlprg.rsession.TestRSession; import org.prlprg.sexp.*; @@ -59,4 +61,39 @@ public void testLgls() throws Exception { fail("Expected LglSXP"); } } + + @Test + public void testReals() throws Exception { + var reals = SEXPs.real(5.2, 4.0, Constants.NA_REAL, 2.0, NaN, 1.0); + var output = new ByteArrayOutputStream(); + + RDSWriter.writeStream(rsession, output, reals); + + var input = new ByteArrayInputStream(output.toByteArray()); + var sexp = RDSReader.readStream(rsession, input); + + if (sexp instanceof RealSXP read_reals) { + assertEquals(6, read_reals.size()); + assertEquals(5.2, read_reals.get(0)); + assertEquals(4.0, read_reals.get(1)); + assertEquals(Constants.NA_REAL, read_reals.get(2)); + assertEquals(2.0, read_reals.get(3)); + assertEquals(NaN, read_reals.get(4)); + assertEquals(1.0, read_reals.get(5)); + } else { + fail("Expected RealSXP"); + } + } + + @Test + public void testNull() throws Exception { + var output = new ByteArrayOutputStream(); + + RDSWriter.writeStream(rsession, output, SEXPs.NULL); + + var input = new ByteArrayInputStream(output.toByteArray()); + var sexp = RDSReader.readStream(rsession, input); + + assertEquals(SEXPs.NULL, sexp); + } } From a133a1cd6d60b0539d11b3f530c17ebb4f00552a Mon Sep 17 00:00:00 2001 From: Pierre Donat-Bouillud Date: Wed, 13 Mar 2024 16:54:08 +0100 Subject: [PATCH 10/31] Tests writing with GNU R as oracle --- src/main/java/org/prlprg/rds/RDSWriter.java | 28 ++++++- .../java/org/prlprg/rds/RDSWriterTest.java | 77 +++++++++++++++++++ src/test/java/org/prlprg/util/GNUR.java | 20 +++++ 3 files changed, 124 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/prlprg/rds/RDSWriter.java b/src/main/java/org/prlprg/rds/RDSWriter.java index 5316cf089..78d760ec8 100644 --- a/src/main/java/org/prlprg/rds/RDSWriter.java +++ b/src/main/java/org/prlprg/rds/RDSWriter.java @@ -32,6 +32,12 @@ public static void writeStream(RSession session, OutputStream output, SEXP sexp) } } + public static void writeFile(RSession session, File file, SEXP sexp) throws IOException { + try (var output = new FileOutputStream(file)) { + writeStream(session, output, sexp); + } + } + public void writeheader(SEXP sexp) throws IOException { // Could also be "B" (binary) and "A" (ASCII) but we only support XDR. // XDR just means big endian and DataInputStream/DataOutputStream from Java use BigEndian @@ -186,6 +192,14 @@ private void writeDottedPairObjects(SEXP s, boolean hasattr) throws IOException writeItem(i); } } + case ListSXP l -> { + for (var i : l) { + assert i.tag() != null; + writeSymbol(i.tag()); + writeItem(i.value()); + } + writeItem(SEXPs.NULL); + } case PromSXP p -> { // a promise has the value, expression and environment, in this order writeItem(p.getVal()); @@ -219,7 +233,11 @@ private int packFlags(int type, int levs, boolean isobj, boolean hasattr, boolea } private void writeAttributes(Attributes attrs) throws IOException { - throw new UnsupportedOperationException("not implemented yet"); + + // convert to ListSXP + var l = attrs.entrySet().stream().map(e -> new TaggedElem(e.getKey(), e.getValue())).toList(); + // Write it + writeItem(SEXPs.list(l)); } private void writeChars(String s) throws IOException { @@ -285,6 +303,14 @@ private void writeBCode(BCodeSXP s) throws IOException { throw new UnsupportedOperationException("not implemented yet"); } + private void writeSymbol(SymSXP s) throws IOException { + throw new UnsupportedOperationException("not implemented yet"); + } + + private void writeSymbol(String s) throws IOException { + throw new UnsupportedOperationException("not implemented yet"); + } + @Override public void close() throws IOException { out.close(); diff --git a/src/test/java/org/prlprg/rds/RDSWriterTest.java b/src/test/java/org/prlprg/rds/RDSWriterTest.java index 127714b13..5b5034a8e 100644 --- a/src/test/java/org/prlprg/rds/RDSWriterTest.java +++ b/src/test/java/org/prlprg/rds/RDSWriterTest.java @@ -42,6 +42,49 @@ public void testInts() throws Exception { } } + @Test + public void testInts_withRef() throws Exception { + var ints = SEXPs.integer(5, 4, 3, 2, 1); + var output = + R.eval( + """ + typeof(input) == "integer" && identical(input, c(5L, 4L, 3L, 2L, 1L)) + """, + ints); + + if (output instanceof LglSXP read_lgls) { + assertEquals(1, read_lgls.size()); + assertEquals(Logical.TRUE, read_lgls.get(0)); + } else { + fail("Expected LglSXP"); + } + } + + @Test + public void testVecAttributes() throws Exception { + var attrs = + new Attributes.Builder() + .put("a", SEXPs.integer(1)) + .put("b", SEXPs.logical(Logical.TRUE)) + .build(); + var ints = SEXPs.integer(1, attrs); + + var output = new ByteArrayOutputStream(); + + RDSWriter.writeStream(rsession, output, ints); + + var input = new ByteArrayInputStream(output.toByteArray()); + var sexp = RDSReader.readStream(rsession, input); + + if (sexp instanceof IntSXP read_ints) { + assertEquals(1, read_ints.size()); + assertEquals(1, read_ints.get(0)); + assertEquals(attrs, read_ints.attributes()); + } else { + fail("Expected IntSXP"); + } + } + @Test public void testLgls() throws Exception { var lgls = SEXPs.logical(Logical.TRUE, Logical.FALSE, Logical.NA); @@ -96,4 +139,38 @@ public void testNull() throws Exception { assertEquals(SEXPs.NULL, sexp); } + + @Test + public void testVec() throws Exception { + var vec = + SEXPs.vec(SEXPs.integer(1, 2, 3), SEXPs.logical(Logical.TRUE, Logical.FALSE, Logical.NA)); + var output = new ByteArrayOutputStream(); + + RDSWriter.writeStream(rsession, output, vec); + + var input = new ByteArrayInputStream(output.toByteArray()); + var sexp = RDSReader.readStream(rsession, input); + + if (sexp instanceof VecSXP read_vec) { + assertEquals(2, read_vec.size()); + if (read_vec.get(0) instanceof IntSXP read_ints) { + assertEquals(3, read_ints.size()); + assertEquals(1, read_ints.get(0)); + assertEquals(2, read_ints.get(1)); + assertEquals(3, read_ints.get(2)); + } else { + fail("Expected IntSXP for the 1st element of the VecSXP"); + } + if (read_vec.get(1) instanceof LglSXP read_lgls) { + assertEquals(3, read_lgls.size()); + assertEquals(Logical.TRUE, read_lgls.get(0)); + assertEquals(Logical.FALSE, read_lgls.get(1)); + assertEquals(Logical.NA, read_lgls.get(2)); + } else { + fail("Expected LglSXP for the 2nd element of the VecSXP"); + } + } else { + fail("Expected VecSXP"); + } + } } diff --git a/src/test/java/org/prlprg/util/GNUR.java b/src/test/java/org/prlprg/util/GNUR.java index f10c9726c..6c330bd49 100644 --- a/src/test/java/org/prlprg/util/GNUR.java +++ b/src/test/java/org/prlprg/util/GNUR.java @@ -4,6 +4,7 @@ import java.io.PrintWriter; import org.prlprg.RSession; import org.prlprg.rds.RDSReader; +import org.prlprg.rds.RDSWriter; import org.prlprg.sexp.SEXP; public class GNUR { @@ -42,6 +43,25 @@ public SEXP eval(String source) { } } + /** + * Evaluate R source with input SEXP. The SEXP is passed from Java to the R world using RDS. + * + * @param source + * @param input + * @return + */ + public SEXP eval(String source, SEXP input) { + try { + var inputFile = File.createTempFile("RCS-input", ".rds"); + RDSWriter.writeFile(rsession, inputFile, input); + String full_source = "input <- readRDS('" + inputFile.getAbsolutePath() + "')\n" + source; + + return eval(full_source); + } catch (Exception e) { + throw new RuntimeException("Unable to eval R source", e); + } + } + private static void runCode(String code) { try { var pb = From f6e61712a5d7b82c798e462200d0efc2e22ce431 Mon Sep 17 00:00:00 2001 From: Pierre Donat-Bouillud Date: Fri, 15 Mar 2024 16:06:19 +0100 Subject: [PATCH 11/31] ListSXP works. Type should be written as int not byte! --- src/main/java/org/prlprg/rds/Flags.java | 14 +++ src/main/java/org/prlprg/rds/RDSWriter.java | 107 ++++++++++++------ .../java/org/prlprg/sexp/LangOrListSXP.java | 7 ++ src/main/java/org/prlprg/sexp/LangSXP.java | 2 +- src/main/java/org/prlprg/sexp/ListSXP.java | 3 +- src/main/java/org/prlprg/sexp/SEXP.java | 9 +- .../java/org/prlprg/rds/RDSWriterTest.java | 42 +++++++ 7 files changed, 148 insertions(+), 36 deletions(-) create mode 100644 src/main/java/org/prlprg/sexp/LangOrListSXP.java diff --git a/src/main/java/org/prlprg/rds/Flags.java b/src/main/java/org/prlprg/rds/Flags.java index c59707ce3..36917e26e 100644 --- a/src/main/java/org/prlprg/rds/Flags.java +++ b/src/main/java/org/prlprg/rds/Flags.java @@ -5,6 +5,8 @@ final class Flags { private static final int ATTR_MASK = 1 << 9; private static final int TAG_MASK = 1 << 10; + private static final int OBJECT_MASK = 1 << 8; + private final int flags; public Flags(int flags) { @@ -20,6 +22,7 @@ public Flags( RDSItemType type, int levels, boolean isUTF8, + boolean isObject, boolean hasAttributes, boolean hasTag, int refIndex) { @@ -27,6 +30,7 @@ public Flags( type.i() | (levels << 12) | (isUTF8 ? UTF8_MASK : 0) + | (isObject ? OBJECT_MASK : 0) | (hasAttributes ? ATTR_MASK : 0) | (hasTag ? TAG_MASK : 0) | (refIndex << 8); @@ -44,6 +48,10 @@ public boolean isUTF8() { return (decodeLevels() & UTF8_MASK) != 0; } + public boolean isObject() { + return (flags & OBJECT_MASK) != 0; + } + public boolean hasAttributes() { return (flags & ATTR_MASK) != 0; } @@ -65,6 +73,8 @@ public String toString() { + decodeLevels() + ", isUTF8=" + isUTF8() + + ", isObject=" + + isObject() + ", hasAttributes=" + hasAttributes() + ", hasTag=" @@ -73,4 +83,8 @@ public String toString() { + unpackRefIndex() + '}'; } + + public int encode() { + return flags; + } } diff --git a/src/main/java/org/prlprg/rds/RDSWriter.java b/src/main/java/org/prlprg/rds/RDSWriter.java index 78d760ec8..eb80ba1ae 100644 --- a/src/main/java/org/prlprg/rds/RDSWriter.java +++ b/src/main/java/org/prlprg/rds/RDSWriter.java @@ -67,6 +67,8 @@ public void write(SEXP sexp) throws IOException { // See // https://github.com/wch/r-source/blob/65892cc124ac20a44950e6e432f9860b1d6e9bf4/src/main/serialize.c#L1021 public void writeItem(SEXP s) throws IOException { + // TODO: write the flags at the beginning. + // ALTREP: TODO // Persisted through the ref table? TODO @@ -113,11 +115,7 @@ public void writeItem(SEXP s) throws IOException { // Symbols if (s instanceof RegSymSXP sym) { - refTable.add(s); - out.writeByte((byte) SEXPType.SYM.i); - // write the name (cannot be NA) - out.writeInt(sym.name().length()); - out.writeString(sym.name()); + writeSymbol(sym); } else if (s.type() == SEXPType.ENV) { refTable.add(s); @@ -136,15 +134,18 @@ else if (s instanceof NamespaceEnvSXP) { } else { // Any other env out.writeInt(SEXPType.ENV.i); out.writeInt(0); // FIXME: should be 1 if locked, 0 if not locked + assert s instanceof UserEnvSXP; + // TODO // Enclosure // Frame // Hashtab // Attributes + writeAttributes(s.attributes()); } } else { boolean hastag = switch (s.type()) { - case LIST, LANG, PROM -> false; // we assume no tag + case LIST, LANG, PROM -> false; // we assume no tag (will be corrected later for List) case CLO -> true; default -> false; }; @@ -154,21 +155,22 @@ else if (s instanceof NamespaceEnvSXP) { && s.attributes() != null && !Objects.requireNonNull(s.attributes()).isEmpty(); // levels in the sxpinfo gp field - int flags = packFlags(s.type().i, 0, s.isObject(), hasattr, hastag); - out.writeInt(flags); + + var rds_type = new RDSItemType.Sexp(s.type()); + var flags = new Flags(rds_type, 0, false, s.isObject(), hasattr, hastag, 0); switch (s) { - case ListSXP l -> writeDottedPairObjects(l, hasattr); - case LangSXP l -> writeDottedPairObjects(l, hasattr); - case PromSXP p -> writeDottedPairObjects(p, hasattr); - case CloSXP c -> writeDottedPairObjects(c, hasattr); + case ListSXP l -> writeDottedPairObjects(l, hasattr, flags); + case LangSXP l -> writeDottedPairObjects(l, hasattr, flags); + case PromSXP p -> writeDottedPairObjects(p, hasattr, flags); + case CloSXP c -> writeDottedPairObjects(c, hasattr, flags); // External pointer // weakreference // special // builtin - case StrSXP str -> writeStrSxp(str, hasattr); - case VectorSXP vec -> writeVector(vec, hasattr); - case BCodeSXP bcode -> writeBCode(bcode); + case StrSXP str -> writeStrSxp(str, hasattr, flags.encode()); + case VectorSXP vec -> writeVector(vec, hasattr, flags.encode()); + case BCodeSXP bcode -> writeBCode(bcode, flags.encode()); default -> throw new UnsupportedOperationException("Unsupported SEXP type: " + s.type()); } @@ -176,27 +178,48 @@ else if (s instanceof NamespaceEnvSXP) { } // Handles LISTSXP (pairlist, LANGSXP, PROMSXP and DOTSXP - private void writeDottedPairObjects(SEXP s, boolean hasattr) throws IOException { + private void writeDottedPairObjects(SEXP s, boolean hasattr, Flags flags) throws IOException { + if (s instanceof ListSXP l && !l.isEmpty() && l.get(0).tag() != null) { + var new_flags = + new Flags(flags.getType(), 0, flags.isUTF8(), flags.isObject(), false, true, 0); + out.writeInt(new_flags.encode()); + } else { + out.writeInt(flags.encode()); + } + // We should write the flag here first // first, attributes if (hasattr) { writeAttributes(Objects.requireNonNull(s.attributes())); } - // tag (except for closures) - // TODO - - // car switch (s) { case LangSXP l -> { - for (var i : l.asList()) { - writeItem(i); - } + // write the fun + writeItem(l.fun()); + // write the args + writeItem(l.args()); } case ListSXP l -> { - for (var i : l) { - assert i.tag() != null; - writeSymbol(i.tag()); - writeItem(i.value()); + int i = 0; + for (var e : l) { + if (i > 0) { + var new_flags = + new Flags( + flags.getType(), + 0, + flags.isUTF8(), + flags.isObject(), + false, + e.tag() != null, + 0); + out.writeInt(new_flags.encode()); + } + + if (e.tag() != null) { + writeSymbol(e.tag()); + } + writeItem(e.value()); + i++; } writeItem(SEXPs.NULL); } @@ -243,12 +266,14 @@ private void writeAttributes(Attributes attrs) throws IOException { private void writeChars(String s) throws IOException { // Never NA because we assume so // We only consider scalar Na strings - // TODO: write flags also here + int flags = packFlags(SEXPType.CHAR.i, 0, false, false, false); + out.writeInt(flags); out.writeInt(s.length()); out.writeString(s); } - private void writeStrSxp(StrSXP s, boolean hasattr) throws IOException { + private void writeStrSxp(StrSXP s, boolean hasattr, int flags) throws IOException { + out.writeInt(flags); out.writeInt(s.size()); for (var str : s) { writeChars(str); @@ -259,7 +284,8 @@ private void writeStrSxp(StrSXP s, boolean hasattr) throws IOException { } } - private void writeVector(VectorSXP s, boolean hasattr) throws IOException { + private void writeVector(VectorSXP s, boolean hasattr, int flags) throws IOException { + out.writeInt(flags); out.writeInt(s.size()); switch (s) { @@ -299,16 +325,31 @@ private void writeVector(VectorSXP s, boolean hasattr) throws IOException } } - private void writeBCode(BCodeSXP s) throws IOException { + private void writeBCode(BCodeSXP s, int flags) throws IOException { + out.writeInt(flags); throw new UnsupportedOperationException("not implemented yet"); } private void writeSymbol(SymSXP s) throws IOException { - throw new UnsupportedOperationException("not implemented yet"); + switch (s) { + case RegSymSXP regSymSXP -> writeSymbol(regSymSXP); + case SpecialSymSXP specialSymSXP when specialSymSXP.isEllipsis() -> { + out.writeByte((byte) SEXPType.SYM.i); + writeChars("..."); // Really? + } + default -> + throw new UnsupportedOperationException("Unreachable: implemented in special sexps."); + } + } + + private void writeSymbol(RegSymSXP s) throws IOException { + refTable.add(s); + writeSymbol(s.name()); } private void writeSymbol(String s) throws IOException { - throw new UnsupportedOperationException("not implemented yet"); + out.writeInt(SEXPType.SYM.i); + writeChars(s); } @Override diff --git a/src/main/java/org/prlprg/sexp/LangOrListSXP.java b/src/main/java/org/prlprg/sexp/LangOrListSXP.java new file mode 100644 index 000000000..108f57779 --- /dev/null +++ b/src/main/java/org/prlprg/sexp/LangOrListSXP.java @@ -0,0 +1,7 @@ +package org.prlprg.sexp; + +import javax.annotation.concurrent.Immutable; + +/** Either {@link ListSXP} (AST identifier) or {@link LangSXP} (AST call). */ +@Immutable +public sealed interface LangOrListSXP extends SEXP permits ListSXP, LangSXP {} diff --git a/src/main/java/org/prlprg/sexp/LangSXP.java b/src/main/java/org/prlprg/sexp/LangSXP.java index 2ccc584a2..319c80f98 100644 --- a/src/main/java/org/prlprg/sexp/LangSXP.java +++ b/src/main/java/org/prlprg/sexp/LangSXP.java @@ -6,7 +6,7 @@ /** AST function call ("language object") SEXP. */ @Immutable -public sealed interface LangSXP extends SymOrLangSXP { +public sealed interface LangSXP extends SymOrLangSXP, LangOrListSXP { /** The function being called. */ SymOrLangSXP fun(); diff --git a/src/main/java/org/prlprg/sexp/ListSXP.java b/src/main/java/org/prlprg/sexp/ListSXP.java index 92c3c0733..8a74b3f04 100644 --- a/src/main/java/org/prlprg/sexp/ListSXP.java +++ b/src/main/java/org/prlprg/sexp/ListSXP.java @@ -13,7 +13,8 @@ * @implNote In GNU-R this is represented as a linked list, but we internally use an array-list * because it's more efficient. */ -public sealed interface ListSXP extends ListOrVectorSXP permits NilSXP, ListSXPImpl { +public sealed interface ListSXP extends ListOrVectorSXP, LangOrListSXP + permits NilSXP, ListSXPImpl { /** * Flatten {@code src} while adding its elements to {@code target}. Ex: * diff --git a/src/main/java/org/prlprg/sexp/SEXP.java b/src/main/java/org/prlprg/sexp/SEXP.java index f52a2a50b..f7989432a 100644 --- a/src/main/java/org/prlprg/sexp/SEXP.java +++ b/src/main/java/org/prlprg/sexp/SEXP.java @@ -11,7 +11,14 @@ * suspect GNU-R SEXPs aren't actually S-expressions. */ public sealed interface SEXP - permits StrOrRegSymSXP, SymOrLangSXP, ListOrVectorSXP, CloSXP, EnvSXP, BCodeSXP, PromSXP { + permits StrOrRegSymSXP, + SymOrLangSXP, + ListOrVectorSXP, + LangOrListSXP, + CloSXP, + EnvSXP, + BCodeSXP, + PromSXP { /** * SEXPTYPE. It's important to distinguish these from the SEXP's class, because there's a class * for every type but not vice versa due to subclasses (e.g. simple-scalar ints have the same diff --git a/src/test/java/org/prlprg/rds/RDSWriterTest.java b/src/test/java/org/prlprg/rds/RDSWriterTest.java index 5b5034a8e..300c5889f 100644 --- a/src/test/java/org/prlprg/rds/RDSWriterTest.java +++ b/src/test/java/org/prlprg/rds/RDSWriterTest.java @@ -173,4 +173,46 @@ public void testVec() throws Exception { fail("Expected VecSXP"); } } + + @Test + public void testList() throws Exception { + var elems = + new TaggedElem[] { + new TaggedElem("a", SEXPs.integer(1)), + new TaggedElem("b", SEXPs.logical(Logical.TRUE)), + new TaggedElem("c", SEXPs.real(3.14, 2.71)) + }; + var list = SEXPs.list(elems, Attributes.NONE); + var output = new ByteArrayOutputStream(); + + RDSWriter.writeStream(rsession, output, list); + + var input = new ByteArrayInputStream(output.toByteArray()); + var sexp = RDSReader.readStream(rsession, input); + + if (sexp instanceof ListSXP l) { + assertEquals(3, l.size()); + assertEquals("a", l.get(0).tag()); + if (l.get(0).value() instanceof IntSXP i) { + assertEquals(1, i.get(0)); + } else { + fail("Expected IntSXP for the 1st element of the ListSXP"); + } + assertEquals("b", l.get(1).tag()); + if (l.get(1).value() instanceof LglSXP lgl) { + assertEquals(Logical.TRUE, lgl.get(0)); + } else { + fail("Expected LglSXP for the 2nd element of the ListSXP"); + } + assertEquals("c", l.get(2).tag()); + if (l.get(2).value() instanceof RealSXP r) { + assertEquals(3.14, r.get(0)); + assertEquals(2.71, r.get(1)); + } else { + fail("Expected RealSXP for the 3rd element of the ListSXP"); + } + } else { + fail("Expected ListSXP"); + } + } } From 6a946a64391a5d06b3a2b85f5e6715bc96741e50 Mon Sep 17 00:00:00 2001 From: Pierre Donat-Bouillud Date: Tue, 19 Mar 2024 17:53:45 +0100 Subject: [PATCH 12/31] Supports user environments --- src/main/java/org/prlprg/rds/RDSWriter.java | 39 +++++++------ src/main/java/org/prlprg/sexp/BaseEnvSXP.java | 5 ++ .../java/org/prlprg/sexp/EmptyEnvSXP.java | 5 ++ src/main/java/org/prlprg/sexp/EnvSXP.java | 7 +++ .../java/org/prlprg/sexp/GlobalEnvSXP.java | 6 ++ .../java/org/prlprg/sexp/NamespaceEnvSXP.java | 6 ++ src/main/java/org/prlprg/sexp/UserEnvSXP.java | 30 +++++++++- .../java/org/prlprg/rds/RDSWriterTest.java | 55 ++++++++++++++++++- 8 files changed, 134 insertions(+), 19 deletions(-) diff --git a/src/main/java/org/prlprg/rds/RDSWriter.java b/src/main/java/org/prlprg/rds/RDSWriter.java index eb80ba1ae..6c73e193e 100644 --- a/src/main/java/org/prlprg/rds/RDSWriter.java +++ b/src/main/java/org/prlprg/rds/RDSWriter.java @@ -6,6 +6,7 @@ import java.util.List; import java.util.Objects; import java.util.stream.StreamSupport; +import javax.annotation.Nullable; import org.prlprg.RSession; import org.prlprg.RVersion; import org.prlprg.primitive.Logical; @@ -118,29 +119,31 @@ public void writeItem(SEXP s) throws IOException { writeSymbol(sym); } else if (s.type() == SEXPType.ENV) { refTable.add(s); - - // Package - // TODO: add PackageEnv to the EnvSXP interface - StrSXP name = (StrSXP) Objects.requireNonNull(s.attributes()).get("name"); - assert name != null; - if (name.get(0).startsWith("package:")) { - out.writeInt(RDSItemType.Special.PACKAGESXP.i()); - writeItem(name); - } // Namespace - else if (s instanceof NamespaceEnvSXP) { + if (s instanceof NamespaceEnvSXP) { out.writeInt(RDSItemType.Special.NAMESPACESXP.i()); // TODO: build STRSXP with name and version throw new UnsupportedOperationException("not implemented yet"); - } else { // Any other env + } else if (s instanceof UserEnvSXP env) { // Any other env out.writeInt(SEXPType.ENV.i); out.writeInt(0); // FIXME: should be 1 if locked, 0 if not locked - assert s instanceof UserEnvSXP; - // TODO // Enclosure + writeItem(env.parent()); // Frame - // Hashtab + writeItem(env.frame()); + // Hashtab (NULL or VECSXP) + writeItem(SEXPs.NULL); // simple version here. + // Otherwise, we would have to actually do the hashing as it is done in R + // Attributes - writeAttributes(s.attributes()); + // R always write something here, as it does not write a hastag bit in the flags + // (it actually has no flags; it just writes the type ENV) + if ((env.attributes() != null) && !Objects.requireNonNull(env.attributes()).isEmpty()) { + writeAttributes(env.attributes()); + } else { + writeItem(SEXPs.NULL); + } + } else { + throw new UnsupportedOperationException("Unsupported env type: " + s.type()); } } else { boolean hastag = @@ -255,8 +258,10 @@ private int packFlags(int type, int levs, boolean isobj, boolean hasattr, boolea return val; } - private void writeAttributes(Attributes attrs) throws IOException { - + private void writeAttributes(@Nullable Attributes attrs) throws IOException { + if (attrs == null || attrs.isEmpty()) { + return; + } // convert to ListSXP var l = attrs.entrySet().stream().map(e -> new TaggedElem(e.getKey(), e.getValue())).toList(); // Write it diff --git a/src/main/java/org/prlprg/sexp/BaseEnvSXP.java b/src/main/java/org/prlprg/sexp/BaseEnvSXP.java index 1b6ef8230..82f3b5e75 100644 --- a/src/main/java/org/prlprg/sexp/BaseEnvSXP.java +++ b/src/main/java/org/prlprg/sexp/BaseEnvSXP.java @@ -26,6 +26,11 @@ public Optional getLocal(String name) { return Optional.ofNullable(bindings.get(name)); } + @Override + public int size() { + return bindings.size(); + } + @Override public String toString() { // TODO: add some link to the R session? diff --git a/src/main/java/org/prlprg/sexp/EmptyEnvSXP.java b/src/main/java/org/prlprg/sexp/EmptyEnvSXP.java index d92a48267..03da8e552 100644 --- a/src/main/java/org/prlprg/sexp/EmptyEnvSXP.java +++ b/src/main/java/org/prlprg/sexp/EmptyEnvSXP.java @@ -24,6 +24,11 @@ public Optional getLocal(String name) { return Optional.empty(); } + @Override + public int size() { + return 0; + } + @Override public Optional> find(String name) { return Optional.empty(); diff --git a/src/main/java/org/prlprg/sexp/EnvSXP.java b/src/main/java/org/prlprg/sexp/EnvSXP.java index faad7074b..9d9a14ac0 100644 --- a/src/main/java/org/prlprg/sexp/EnvSXP.java +++ b/src/main/java/org/prlprg/sexp/EnvSXP.java @@ -29,6 +29,13 @@ public sealed interface EnvSXP extends SEXP */ Optional getLocal(String name); + /** + * Get the number of symbols in the environment (locally) + * + * @return the number of symbols in the environment + */ + int size(); + @Override default SEXPType type() { return SEXPType.ENV; diff --git a/src/main/java/org/prlprg/sexp/GlobalEnvSXP.java b/src/main/java/org/prlprg/sexp/GlobalEnvSXP.java index 4c15a65ca..53d005329 100644 --- a/src/main/java/org/prlprg/sexp/GlobalEnvSXP.java +++ b/src/main/java/org/prlprg/sexp/GlobalEnvSXP.java @@ -30,6 +30,12 @@ public Optional getLocal(String name) { return Optional.empty(); } + @Override + public int size() { + // FIXME: implement + return 0; + } + @Override public String toString() { // TODO: add some link to the R session? diff --git a/src/main/java/org/prlprg/sexp/NamespaceEnvSXP.java b/src/main/java/org/prlprg/sexp/NamespaceEnvSXP.java index 774747e4d..aeaea41de 100644 --- a/src/main/java/org/prlprg/sexp/NamespaceEnvSXP.java +++ b/src/main/java/org/prlprg/sexp/NamespaceEnvSXP.java @@ -30,6 +30,12 @@ public Optional getLocal(String name) { return Optional.empty(); } + @Override + public int size() { + // FIXME: implement + return 0; + } + public String getVersion() { return version; } diff --git a/src/main/java/org/prlprg/sexp/UserEnvSXP.java b/src/main/java/org/prlprg/sexp/UserEnvSXP.java index 7db31298e..764050c43 100644 --- a/src/main/java/org/prlprg/sexp/UserEnvSXP.java +++ b/src/main/java/org/prlprg/sexp/UserEnvSXP.java @@ -1,11 +1,14 @@ package org.prlprg.sexp; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterators; import java.util.HashMap; +import java.util.Iterator; import java.util.Map; import java.util.Optional; import javax.annotation.Nullable; -public final class UserEnvSXP implements EnvSXP { +public final class UserEnvSXP implements EnvSXP, Iterable { private EnvSXP parent; private final Map entries; private @Nullable Attributes attributes; @@ -24,6 +27,21 @@ public EnvSXP parent() { return parent; } + public Iterator iterator() { + // We need to transform the entries to TaggedElem to avoid exposing the internal map. + // This is not very efficient though. + return Iterators.transform( + entries.entrySet().iterator(), e -> new TaggedElem(e.getKey(), e.getValue())); + } + + public ListSXP frame() { + return new ListSXPImpl( + entries.entrySet().stream() + .map(e -> new TaggedElem(e.getKey(), e.getValue())) + .collect(ImmutableList.toImmutableList()), + Attributes.NONE); + } + @Override public Optional get(String name) { return getLocal(name).or(() -> parent.get(name)); @@ -34,6 +52,11 @@ public Optional getLocal(String name) { return Optional.ofNullable(entries.get(name)); } + @Override + public int size() { + return entries.size(); + } + @Override public String toString() { return ""; @@ -45,6 +68,11 @@ public UserEnvSXP withAttributes(Attributes attributes) { return this; } + @Override + public Attributes attributes() { + return attributes; + } + public void setParent(EnvSXP parent) { this.parent = parent; } diff --git a/src/test/java/org/prlprg/rds/RDSWriterTest.java b/src/test/java/org/prlprg/rds/RDSWriterTest.java index 300c5889f..9d86a794b 100644 --- a/src/test/java/org/prlprg/rds/RDSWriterTest.java +++ b/src/test/java/org/prlprg/rds/RDSWriterTest.java @@ -43,7 +43,7 @@ public void testInts() throws Exception { } @Test - public void testInts_withRef() throws Exception { + public void testInts_withR() throws Exception { var ints = SEXPs.integer(5, 4, 3, 2, 1); var output = R.eval( @@ -215,4 +215,57 @@ public void testList() throws Exception { fail("Expected ListSXP"); } } + + @Test + public void testEnv() throws Exception { + var env = new UserEnvSXP(); + env.set("a", SEXPs.integer(1)); + env.set("b", SEXPs.logical(Logical.TRUE)); + env.set("c", SEXPs.real(3.14, 2.71)); + env.set("d", SEXPs.string("foo", "bar")); + env.setAttributes(new Attributes.Builder().put("test", SEXPs.logical(Logical.TRUE)).build()); + + var output = new ByteArrayOutputStream(); + + RDSWriter.writeStream(rsession, output, env); + + var input = new ByteArrayInputStream(output.toByteArray()); + var sexp = RDSReader.readStream(rsession, input); + + if (sexp instanceof UserEnvSXP read_env) { + assertEquals(4, read_env.size()); + assertEquals(SEXPs.integer(1), read_env.get("a").orElseThrow()); + assertEquals(SEXPs.logical(Logical.TRUE), read_env.get("b").orElseThrow()); + assertEquals(SEXPs.real(3.14, 2.71), read_env.get("c").orElseThrow()); + assertEquals(SEXPs.string("foo", "bar"), read_env.get("d").orElseThrow()); + assertEquals( + new Attributes.Builder().put("test", SEXPs.logical(Logical.TRUE)).build(), + read_env.attributes()); + } else { + fail("Expected UserEnvSXP"); + } + } + + @Test + public void testEnv_withR() throws Exception { + var env = new UserEnvSXP(); + env.set("a", SEXPs.integer(1)); + env.set("b", SEXPs.logical(Logical.TRUE)); + env.set("c", SEXPs.real(3.14, 2.71)); + env.set("d", SEXPs.string("foo", "bar")); + + var output = + R.eval( + """ + typeof(input) == "environment" #&& input$a == 1L && input$b == TRUE && identical(input$c, c(3.14, 2.71)) && identical(input$d, c("foo", "bar")) + """, + env); + + if (output instanceof LglSXP read_lgls) { + assertEquals(1, read_lgls.size()); + assertEquals(Logical.TRUE, read_lgls.get(0)); + } else { + fail("Expected LglSXP"); + } + } } From f4c144ed24b828263f1b34d3d61b0c21f1ab0716 Mon Sep 17 00:00:00 2001 From: Pierre Donat-Bouillud Date: Thu, 21 Mar 2024 18:35:03 +0100 Subject: [PATCH 13/31] Bytecode serialization: WIP --- src/main/java/org/prlprg/bc/BcCode.java | 12 ++++ src/main/java/org/prlprg/rds/RDSWriter.java | 74 +++++++++++++++++++-- 2 files changed, 81 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/prlprg/bc/BcCode.java b/src/main/java/org/prlprg/bc/BcCode.java index 2b502ddad..df078dd0c 100644 --- a/src/main/java/org/prlprg/bc/BcCode.java +++ b/src/main/java/org/prlprg/bc/BcCode.java @@ -99,6 +99,18 @@ public String toString() { return sb.toString(); } + public ImmutableIntArray toRaw() { + var builder = ImmutableIntArray.builder(); + builder.add(Bc.R_BC_VERSION); + for (var instr : code) { + builder.add(instr.op().value()); + for (var i = 0; i < instr.op().nArgs(); i++) { + builder.add(instr.op().nArgs()); + } + } + return builder.build(); + } + /** * A builder class for creating BcArray instances. * diff --git a/src/main/java/org/prlprg/rds/RDSWriter.java b/src/main/java/org/prlprg/rds/RDSWriter.java index 6c73e193e..b605bb2de 100644 --- a/src/main/java/org/prlprg/rds/RDSWriter.java +++ b/src/main/java/org/prlprg/rds/RDSWriter.java @@ -2,9 +2,7 @@ import java.io.*; import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; +import java.util.*; import java.util.stream.StreamSupport; import javax.annotation.Nullable; import org.prlprg.RSession; @@ -68,8 +66,6 @@ public void write(SEXP sexp) throws IOException { // See // https://github.com/wch/r-source/blob/65892cc124ac20a44950e6e432f9860b1d6e9bf4/src/main/serialize.c#L1021 public void writeItem(SEXP s) throws IOException { - // TODO: write the flags at the beginning. - // ALTREP: TODO // Persisted through the ref table? TODO @@ -167,6 +163,7 @@ public void writeItem(SEXP s) throws IOException { case LangSXP l -> writeDottedPairObjects(l, hasattr, flags); case PromSXP p -> writeDottedPairObjects(p, hasattr, flags); case CloSXP c -> writeDottedPairObjects(c, hasattr, flags); + // ComplexSXP // External pointer // weakreference // special @@ -330,8 +327,75 @@ private void writeVector(VectorSXP s, boolean hasattr, int flags) throws } } + private void scanForCircles(SEXP sexp, HashSet nobcodes) { + switch (sexp) { + case LangOrListSXP l -> { + if (!nobcodes.contains(l)) { + nobcodes.add(l); + for (var e : + switch (l) { + case LangSXP lang -> lang.args(); + case ListSXP list -> list; + }) { + scanForCircles(e.value(), nobcodes); + } + } + } + case BCodeSXP bc -> { + for (var e : bc.bc().consts()) { + scanForCircles(e, nobcodes); + } + } + default -> throw new RuntimeException("Unexpected sexp type: " + sexp.type()); + } + } + + private void writeBCLang(SEXP s, HashSet reps) throws IOException { + throw new UnsupportedOperationException("not implemented yet"); + } + + private void writeBC1(BCodeSXP s, HashSet reps) throws IOException { + var code_bytes = s.bc().code().toRaw(); + writeItem(SEXPs.integer(code_bytes)); + + var consts = s.bc().consts(); + out.writeInt(consts.size()); + + // Iterate the constant pool and write the values + for (var c : consts) { + switch (c) { + case BCodeSXP bc -> { + out.writeInt(c.type().i); + writeBC1(bc, reps); + } + case LangOrListSXP l -> { + writeBCLang(l, reps); + } + default -> { + out.writeInt(c.type().i); + writeItem(c); + } + } + } + } + private void writeBCode(BCodeSXP s, int flags) throws IOException { out.writeInt(flags); + + // Scan for circles + // prepend the result it with a scalar integer starting with 1 + var nobcodes = new HashSet(); + scanForCircles(s, nobcodes); + out.writeInt(nobcodes.size() + 1); + + // Decode the bytecode (we will get a vector of integers) + // write the vector of integers + + // write the number of consts in the bytecode + // iterate the consts: if it s bytecode, write the type and recurse + // if it is langsxp or listsxp, write them , using the BCREDPEF, ATTRALANGSXP and ATTRLISTSXP + // else write the type and the value + throw new UnsupportedOperationException("not implemented yet"); } From da47ea410a29b2387760ca9b93be05cc46aa6f0b Mon Sep 17 00:00:00 2001 From: Pierre Donat-Bouillud Date: Mon, 27 May 2024 17:46:01 +0200 Subject: [PATCH 14/31] Some stubs I was starting to fill for the bytecode writer --- src/main/java/org/prlprg/rds/RDSWriter.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/main/java/org/prlprg/rds/RDSWriter.java b/src/main/java/org/prlprg/rds/RDSWriter.java index b605bb2de..e115a2b82 100644 --- a/src/main/java/org/prlprg/rds/RDSWriter.java +++ b/src/main/java/org/prlprg/rds/RDSWriter.java @@ -350,7 +350,20 @@ private void scanForCircles(SEXP sexp, HashSet nobcodes) { } } + // FIXME: replace HashSet by HashTable, and store as value the reference number private void writeBCLang(SEXP s, HashSet reps) throws IOException { + switch (s) { + case LangOrListSXP l -> { + if (!reps.contains((l))) { + out.writeInt(RDSItemType.Special.BCREPDEF.i()); + + } else { + // already seen to we put the index + } + // TODO + } + default -> throw new UnsupportedOperationException("not implemented yet"); + } throw new UnsupportedOperationException("not implemented yet"); } From 71bce82d6d57726424151dace455e57b8857021f Mon Sep 17 00:00:00 2001 From: Nicholas Breitling Date: Tue, 18 Jun 2024 18:27:05 +0200 Subject: [PATCH 15/31] Support for complex number serialization --- .idea/codeStyles/Project.xml | 5 +++ src/main/java/org/prlprg/rds/RDSWriter.java | 17 ++++++--- .../java/org/prlprg/rds/RDSWriterTest.java | 37 +++++++++++++++++++ 3 files changed, 53 insertions(+), 6 deletions(-) diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml index edd5ef344..8dc202820 100644 --- a/.idea/codeStyles/Project.xml +++ b/.idea/codeStyles/Project.xml @@ -4,6 +4,11 @@