Add YANG IR I/O support 60/92260/24
authorRobert Varga <robert.varga@pantheon.tech>
Mon, 24 Aug 2020 16:36:00 +0000 (18:36 +0200)
committerRobert Varga <robert.varga@pantheon.tech>
Thu, 21 Dec 2023 10:26:31 +0000 (11:26 +0100)
Persisting the intermediate representation and restoring it is much
faster and memory-efficient than parsing YANG files from scratch.

This patch adds the smarts to persist/restore an IRStatement to/from
an arbitrary DataOutput/DataInput.

We also complete AbstractIRObject definitions by adding appropriate
hashCode()/equals() methods.

JIRA: YANGTOOLS-1461
Change-Id: Iff10b91c8439d3323ae8c263480f4dfd6c1f1c00
Signed-off-by: Robert Varga <robert.varga@pantheon.tech>
parser/yang-ir/src/main/java/org/opendaylight/yangtools/yang/ir/IOConstantsV1.java [new file with mode: 0644]
parser/yang-ir/src/main/java/org/opendaylight/yangtools/yang/ir/IOSupport.java [new file with mode: 0644]
parser/yang-ir/src/main/java/org/opendaylight/yangtools/yang/ir/IRStatement.java
parser/yang-ir/src/main/java/org/opendaylight/yangtools/yang/ir/StatementInput.java [new file with mode: 0644]
parser/yang-ir/src/main/java/org/opendaylight/yangtools/yang/ir/StatementInputV1.java [new file with mode: 0644]
parser/yang-ir/src/main/java/org/opendaylight/yangtools/yang/ir/StatementOutput.java [new file with mode: 0644]
parser/yang-ir/src/main/java/org/opendaylight/yangtools/yang/ir/StatementOutputV1.java [new file with mode: 0644]
parser/yang-parser-rfc7950/src/test/java/org/opendaylight/yangtools/yang/parser/rfc7950/antlr/IOSupportTest.java [new file with mode: 0644]
parser/yang-parser-rfc7950/src/test/java/org/opendaylight/yangtools/yang/stmt/TestUtils.java

diff --git a/parser/yang-ir/src/main/java/org/opendaylight/yangtools/yang/ir/IOConstantsV1.java b/parser/yang-ir/src/main/java/org/opendaylight/yangtools/yang/ir/IOConstantsV1.java
new file mode 100644 (file)
index 0000000..07016ba
--- /dev/null
@@ -0,0 +1,96 @@
+/*
+ * Copyright (c) 2022 PANTHEON.tech, s.r.o. and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.yangtools.yang.ir;
+
+/**
+ * Simplistic coding, without any real magic bit reuse. The idea is that each statement is in the form:
+ * <pre>{@code HEADER LINE COLUMN KEYWORD [ARGUMENT] [SUBSTATEMENTS]}</pre>
+ *
+ * <p>
+ * The {@code HEADER} is always a single byte, internally composed of four bitfields:
+ * <pre>
+ * +---+---+---+---+---+---+---+---+
+ * |  KW TYPE  | SIZE  |ARG|  L6N  |
+ * +---+---+---+---+---+---+---+---+
+ * </pre>
+ *
+ * <p>
+ * The {@code LINE} and {@code COLUMN} are variable size, indicated by the {@code L6N} bits:
+ * <ul>
+ *   <li>{@link #HDR_LOCATION_22} indicates u16 for LINE and u16 for COLUMN</li>
+ *   <li>{@link #HDR_LOCATION_31} indicates u24 for LINE and u8 for COLUMN</li>
+ *   <li>{@link #HDR_LOCATION_44} indicates s32 for LINE and s32 for COLUMN</li>
+ * </ul>
+ *
+ * <p>
+ * The {@code KEYWORD} is variable-format based on {@code KW TYPE} bits:
+ * <ul>
+ *   <li>{@link #HDR_KEY_DEF_QUAL} indicates a new definition, which is composed of two {@code STRING}s</li>
+ *   <li>{@link #HDR_KEY_DEF_UQUAL} indicates a new definition, which is composed of a single {@code STRING}</li>
+ *   <li>{@link #HDR_KEY_REF_U8} indicates a reference identified by a u8 integer</li>
+ *   <li>{@link #HDR_KEY_REF_U16} indicates a reference identified by a u16 integer</li>
+ *   <li>{@link #HDR_KEY_REF_S32} indicates a reference identified by a s32 integer</li>
+ * </ul>
+ * Once defined, each keyword can be referenced by encoding a reference with a linear counter of definition. I.e.
+ * the first definition is {@code 0}, the second is {@code 1}, etc.
+ *
+ * <p>
+ * The {@code ARGUMENT} is present only when indicated by {@link #HDR_ARGUMENT_PRESENT}. If it is present, it has
+ * variable encoding in form
+ * <pre>{@code ARGHDR [...]}</pre>
+ */
+final class IOConstantsV1 {
+    // Statement indicator: indicates line/column split
+    static final int HDR_LOCATION_22       = 0x01;
+    static final int HDR_LOCATION_31       = 0x02;
+    static final int HDR_LOCATION_44       = 0x03;
+    static final int HDR_LOCATION_MASK     = HDR_LOCATION_44;
+    // Argument presence
+    static final int HDR_ARGUMENT_ABSENT   = 0x00;
+    static final int HDR_ARGUMENT_PRESENT  = 0x04;
+    static final int HDR_ARGUMENT_MASK     = HDR_ARGUMENT_PRESENT;
+    // Child statement size
+    static final int HDR_SIZE_0            = 0x00;
+    static final int HDR_SIZE_U8           = 0x08;
+    static final int HDR_SIZE_U16          = 0x10;
+    static final int HDR_SIZE_S32          = 0x18;
+    static final int HDR_SIZE_MASK         = HDR_SIZE_S32;
+    // Keyword indication
+    static final int HDR_KEY_DEF_UQUAL     = 0x00;
+    static final int HDR_KEY_DEF_QUAL      = 0x20;
+    // 0x40 reserved
+    // 0x60 reserved
+    // 0x80 reserved
+    static final int HDR_KEY_REF_U8        = 0xA0;
+    static final int HDR_KEY_REF_U16       = 0xC0;
+    static final int HDR_KEY_REF_S32       = 0xE0;
+    static final int HDR_KEY_MASK          = HDR_KEY_REF_S32;
+
+    static final int ARG_TYPE_IDENTIFIER   = 0x01;
+    static final int ARG_TYPE_DQUOT        = 0x02;
+    static final int ARG_TYPE_SQUOT        = 0x03;
+    static final int ARG_TYPE_UQUOT        = 0x04;
+    static final int ARG_TYPE_CONCAT_U8    = 0x05;
+    static final int ARG_TYPE_CONCAT_U16   = 0x06;
+    static final int ARG_TYPE_CONCAT_S32   = 0x07;
+    static final int ARG_TYPE_MASK         = ARG_TYPE_CONCAT_S32;
+
+    static final int STR_DEF_UTF           = 0x00; // writeUTF(), <16384
+    static final int STR_DEF_U8            = 0x10; // byte + UTF
+    static final int STR_DEF_U16           = 0x20; // short + UTF
+    static final int STR_DEF_S32           = 0x30; // int + UTF
+    static final int STR_DEF_CHARS         = 0x40; // writeChars()
+    static final int STR_REF_U8            = 0x50;
+    static final int STR_REF_U16           = 0x60;
+    static final int STR_REF_S32           = 0x70;
+    static final int STR_MASK              = STR_REF_S32;
+
+    private IOConstantsV1() {
+        // Hidden on purpose
+    }
+}
diff --git a/parser/yang-ir/src/main/java/org/opendaylight/yangtools/yang/ir/IOSupport.java b/parser/yang-ir/src/main/java/org/opendaylight/yangtools/yang/ir/IOSupport.java
new file mode 100644 (file)
index 0000000..82f9c23
--- /dev/null
@@ -0,0 +1,44 @@
+/*
+ * Copyright (c) 2022 PANTHEON.tech, s.r.o. and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.yangtools.yang.ir;
+
+import com.google.common.annotations.Beta;
+import java.io.DataInput;
+import java.io.DataOutput;
+import java.io.IOException;
+import org.eclipse.jdt.annotation.NonNull;
+
+@Beta
+public final class IOSupport {
+    private static final int MAGICK = 0xAF57BA07;
+
+    private IOSupport() {
+        // Hidden on purpose
+    }
+
+    public static void writeStatement(final DataOutput out, final IRStatement statement) throws IOException {
+        out.writeInt(MAGICK);
+        out.writeByte(1);
+        new StatementOutputV1(out).writeStatement(statement);
+    }
+
+    public static @NonNull IRStatement readStatement(final DataInput in) throws IOException {
+        final int magic = in.readInt();
+        if (magic != MAGICK) {
+            throw new IOException("Unexpected magic " + Integer.toHexString(magic));
+        }
+
+        final int version = in.readUnsignedByte();
+        final var input = switch (version) {
+            case 1 -> new StatementInputV1(in);
+            default -> throw new IOException("Unsupported version " + version);
+        };
+
+        return input.readStatement();
+    }
+}
index 4b5eaf7219dc4dbfac5ae155c416fdf6dfbe8a35..9af542c3c57b297b8439049f20d1f95c4d92ea05 100644 (file)
@@ -26,7 +26,7 @@ import org.eclipse.jdt.annotation.Nullable;
  */
 @Beta
 public abstract sealed class IRStatement extends AbstractIRObject {
-    private static final class Z22 extends IRStatement {
+    static final class Z22 extends IRStatement {
         private final short startLine;
         private final short startColumn;
 
@@ -47,12 +47,16 @@ public abstract sealed class IRStatement extends AbstractIRObject {
         }
     }
 
-    private static final class Z31 extends IRStatement {
+    static final class Z31 extends IRStatement {
         private final int value;
 
         Z31(final IRKeyword keyword, final IRArgument argument, final int startLine, final int startColumn) {
+            this(keyword, argument, startLine << 8 | startColumn & 0xFF);
+        }
+
+        Z31(final IRKeyword keyword, final IRArgument argument, final int value) {
             super(keyword, argument);
-            value = startLine << 8 | startColumn & 0xFF;
+            this.value = value;
         }
 
         @Override
@@ -64,9 +68,13 @@ public abstract sealed class IRStatement extends AbstractIRObject {
         public int startColumn() {
             return value & 0xFF;
         }
+
+        int value() {
+            return value;
+        }
     }
 
-    private static sealed class Z44 extends IRStatement permits O44, L44 {
+    static sealed class Z44 extends IRStatement permits O44, L44 {
         private final int startLine;
         private final int startColumn;
 
@@ -87,7 +95,7 @@ public abstract sealed class IRStatement extends AbstractIRObject {
         }
     }
 
-    private static final class O44 extends Z44 {
+    static final class O44 extends Z44 {
         private final @NonNull IRStatement statement;
 
         O44(final IRKeyword keyword, final IRArgument argument, final IRStatement statement, final int startLine,
@@ -102,7 +110,7 @@ public abstract sealed class IRStatement extends AbstractIRObject {
         }
     }
 
-    private static final class L44 extends Z44 {
+    static final class L44 extends Z44 {
         private final @NonNull ImmutableList<IRStatement> statements;
 
         L44(final IRKeyword keyword, final IRArgument argument, final ImmutableList<IRStatement> statements,
diff --git a/parser/yang-ir/src/main/java/org/opendaylight/yangtools/yang/ir/StatementInput.java b/parser/yang-ir/src/main/java/org/opendaylight/yangtools/yang/ir/StatementInput.java
new file mode 100644 (file)
index 0000000..ba7f376
--- /dev/null
@@ -0,0 +1,24 @@
+/*
+ * Copyright (c) 2022 PANTHEON.tech, s.r.o. and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.yangtools.yang.ir;
+
+import static java.util.Objects.requireNonNull;
+
+import java.io.DataInput;
+import java.io.IOException;
+import org.eclipse.jdt.annotation.NonNull;
+
+abstract class StatementInput {
+    final DataInput in;
+
+    StatementInput(final DataInput in) {
+        this.in = requireNonNull(in);
+    }
+
+    abstract @NonNull IRStatement readStatement() throws IOException;
+}
diff --git a/parser/yang-ir/src/main/java/org/opendaylight/yangtools/yang/ir/StatementInputV1.java b/parser/yang-ir/src/main/java/org/opendaylight/yangtools/yang/ir/StatementInputV1.java
new file mode 100644 (file)
index 0000000..76ce3ad
--- /dev/null
@@ -0,0 +1,204 @@
+/*
+ * Copyright (c) 2022 PANTHEON.tech, s.r.o. and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.yangtools.yang.ir;
+
+import com.google.common.collect.ImmutableList;
+import java.io.DataInput;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+import org.eclipse.jdt.annotation.NonNull;
+import org.eclipse.jdt.annotation.Nullable;
+import org.opendaylight.yangtools.yang.ir.IRArgument.Single;
+import org.opendaylight.yangtools.yang.ir.IRKeyword.Qualified;
+import org.opendaylight.yangtools.yang.ir.IRKeyword.Unqualified;
+
+final class StatementInputV1 extends StatementInput {
+    private final List<@NonNull IRKeyword> keywords = new ArrayList<>();
+    private final List<@NonNull String> strings = new ArrayList<>();
+
+    StatementInputV1(final DataInput in) {
+        super(in);
+    }
+
+    @Override
+    IRStatement readStatement() throws IOException {
+        final int header = in.readUnsignedByte();
+        final int locationBits = header & IOConstantsV1.HDR_LOCATION_MASK;
+        return switch (locationBits) {
+            case IOConstantsV1.HDR_LOCATION_22 -> {
+                final int startLine = in.readUnsignedShort();
+                final int startColumn = in.readUnsignedShort();
+                yield new IRStatement.Z22(readKeyword(header), readArgument(header), startLine, startColumn);
+            }
+            case IOConstantsV1.HDR_LOCATION_31 -> {
+                final int value = in.readInt();
+                yield new IRStatement.Z31(readKeyword(header), readArgument(header), value);
+            }
+            case IOConstantsV1.HDR_LOCATION_44 -> readStatement(header);
+            default -> throw new IOException("Unhandled location " + Integer.toHexString(locationBits));
+        };
+    }
+
+    private @NonNull IRStatement readStatement(final int header) throws IOException {
+        final int startLine = in.readInt();
+        final int startColumn = in.readInt();
+        final var keyword = readKeyword(header);
+        final var argument = readArgument(header);
+        final var statements = readSubstatements(header);
+
+        return switch (statements.size()) {
+            case 0 -> new IRStatement.Z44(keyword, argument, startLine, startColumn);
+            case 1 -> new IRStatement.O44(keyword, argument, statements.get(0), startLine, startColumn);
+            default -> new IRStatement.L44(keyword, argument, statements, startLine, startColumn);
+        };
+    }
+
+    private @NonNull IRKeyword readKeyword(final int header) throws IOException {
+        final int keyBits = header & IOConstantsV1.HDR_KEY_MASK;
+        return switch (keyBits) {
+            case IOConstantsV1.HDR_KEY_REF_U8 -> lookupKeyword(in.readUnsignedByte());
+            case IOConstantsV1.HDR_KEY_REF_U16 -> lookupKeyword(in.readUnsignedShort());
+            case IOConstantsV1.HDR_KEY_REF_S32 -> lookupKeyword(in.readInt());
+            case IOConstantsV1.HDR_KEY_DEF_QUAL -> defineKeyword(Qualified.of(readString(), readString()));
+            case IOConstantsV1.HDR_KEY_DEF_UQUAL -> defineKeyword(Unqualified.of(readString()));
+            default -> throw new IllegalStateException("Unhandled key " + Integer.toHexString(keyBits));
+        };
+    }
+
+    private @NonNull IRKeyword lookupKeyword(final int code) throws IOException {
+        try {
+            return keywords.get(code);
+        } catch (IndexOutOfBoundsException e) {
+            throw new IOException("Failed to look up keyword", e);
+        }
+    }
+
+    private @NonNull IRKeyword defineKeyword(final @NonNull IRKeyword keyword) {
+        keywords.add(keyword);
+        return keyword;
+    }
+
+    private ImmutableList<IRStatement> readSubstatements(final int header) throws IOException {
+        final int sizeBits = header & IOConstantsV1.HDR_SIZE_MASK;
+        return switch (sizeBits) {
+            case IOConstantsV1.HDR_SIZE_0 -> ImmutableList.of();
+            case IOConstantsV1.HDR_SIZE_U8 -> readSubstatementList(in.readUnsignedByte());
+            case IOConstantsV1.HDR_SIZE_U16 -> readSubstatementList(in.readUnsignedShort());
+            case IOConstantsV1.HDR_SIZE_S32 -> readSubstatementList(in.readInt());
+            default -> throw new IOException("Unhandled size " + Integer.toHexString(sizeBits));
+        };
+    }
+
+    private ImmutableList<IRStatement> readSubstatementList(final int size) throws IOException {
+        final var builder = ImmutableList.<IRStatement>builderWithExpectedSize(size);
+        for (int i = 0; i < size; ++i) {
+            builder.add(readStatement());
+        }
+        return builder.build();
+    }
+
+    private @Nullable IRArgument readArgument(final int header) throws IOException {
+        if ((header & IOConstantsV1.HDR_ARGUMENT_MASK) == IOConstantsV1.HDR_ARGUMENT_ABSENT) {
+            return null;
+        }
+
+        final int argHeader = in.readUnsignedByte();
+        final int type = argHeader & IOConstantsV1.ARG_TYPE_MASK;
+        return switch (type) {
+            case IOConstantsV1.ARG_TYPE_IDENTIFIER -> IRArgument.identifier(readString(argHeader));
+            case IOConstantsV1.ARG_TYPE_DQUOT -> IRArgument.doubleQuoted(readString(argHeader));
+            case IOConstantsV1.ARG_TYPE_SQUOT -> IRArgument.singleQuoted(readString(argHeader));
+            case IOConstantsV1.ARG_TYPE_UQUOT -> IRArgument.unquoted(readString(argHeader));
+            case IOConstantsV1.ARG_TYPE_CONCAT_U8 -> readConcatenation(in.readUnsignedByte());
+            case IOConstantsV1.ARG_TYPE_CONCAT_U16 -> readConcatenation(in.readUnsignedShort());
+            case IOConstantsV1.ARG_TYPE_CONCAT_S32 -> readConcatenation(in.readInt());
+            default -> throw new IOException("Unhandled argument " + Integer.toHexString(type));
+        };
+    }
+
+    private @NonNull Single readSingleArgument() throws IOException {
+        final int argHeader = in.readUnsignedByte();
+        final int type = argHeader & IOConstantsV1.ARG_TYPE_MASK;
+        return switch (type) {
+            case IOConstantsV1.ARG_TYPE_IDENTIFIER -> IRArgument.identifier(readString(argHeader));
+            case IOConstantsV1.ARG_TYPE_DQUOT -> IRArgument.doubleQuoted(readString(argHeader));
+            case IOConstantsV1.ARG_TYPE_SQUOT -> IRArgument.singleQuoted(readString(argHeader));
+            case IOConstantsV1.ARG_TYPE_UQUOT -> IRArgument.unquoted(readString(argHeader));
+            default -> throw new IOException("Unhandled single argument " + Integer.toHexString(type));
+        };
+    }
+
+    private @NonNull IRArgument readConcatenation(final int count) throws IOException {
+        final var builder = ImmutableList.<Single>builderWithExpectedSize(count);
+        for (int i = 0; i < count; ++i) {
+            builder.add(readSingleArgument());
+        }
+        return IRArgument.of(builder.build());
+    }
+
+    private @NonNull String readString() throws IOException {
+        return readString(in.readUnsignedByte());
+    }
+
+    private @NonNull String readString(final int header) throws IOException {
+        final int type = header & IOConstantsV1.STR_MASK;
+        return switch (type) {
+            case IOConstantsV1.STR_DEF_UTF -> defineString(in.readUTF());
+            case IOConstantsV1.STR_DEF_U8 -> defineString(readStringBytes(in.readUnsignedByte()));
+            case IOConstantsV1.STR_DEF_U16 -> defineString(readStringBytes(in.readUnsignedShort()));
+            case IOConstantsV1.STR_DEF_S32 -> defineString(readStringBytes(in.readInt()));
+            case IOConstantsV1.STR_DEF_CHARS -> defineString(readStringChars());
+            case IOConstantsV1.STR_REF_U8 -> lookupString(in.readUnsignedByte());
+            case IOConstantsV1.STR_REF_U16 -> lookupString(in.readUnsignedShort());
+            case IOConstantsV1.STR_REF_S32 -> lookupString(in.readInt());
+            default -> throw new IOException("Unhandled string " + Integer.toHexString(type));
+        };
+    }
+
+    private @NonNull String lookupString(final int offset) throws IOException {
+        try {
+            return strings.get(offset);
+        } catch (IndexOutOfBoundsException e) {
+            throw new IOException("Invalid String reference " + offset, e);
+        }
+    }
+
+    private @NonNull String defineString(final @NonNull String string) {
+        strings.add(string);
+        return string;
+    }
+
+    private @NonNull String readStringBytes(final int size) throws IOException {
+        if (size > 0) {
+            final byte[] bytes = new byte[size];
+            in.readFully(bytes);
+            return new String(bytes, StandardCharsets.UTF_8);
+        } else if (size == 0) {
+            return "";
+        } else {
+            throw new IOException("Invalid String bytes length " + size);
+        }
+    }
+
+    private @NonNull String readStringChars() throws IOException {
+        final int size = in.readInt();
+        if (size > 0) {
+            final char[] chars = new char[size];
+            for (int i = 0; i < size; ++i) {
+                chars[i] = in.readChar();
+            }
+            return String.valueOf(chars);
+        } else if (size == 0) {
+            return "";
+        } else {
+            throw new IOException("Invalid String chars length " + size);
+        }
+    }
+}
diff --git a/parser/yang-ir/src/main/java/org/opendaylight/yangtools/yang/ir/StatementOutput.java b/parser/yang-ir/src/main/java/org/opendaylight/yangtools/yang/ir/StatementOutput.java
new file mode 100644 (file)
index 0000000..0cb0804
--- /dev/null
@@ -0,0 +1,23 @@
+/*
+ * Copyright (c) 2022 PANTHEON.tech, s.r.o. and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.yangtools.yang.ir;
+
+import static java.util.Objects.requireNonNull;
+
+import java.io.DataOutput;
+import java.io.IOException;
+
+abstract class StatementOutput {
+    final DataOutput out;
+
+    StatementOutput(final DataOutput out) {
+        this.out = requireNonNull(out);
+    }
+
+    abstract void writeStatement(IRStatement stmt) throws IOException;
+}
diff --git a/parser/yang-ir/src/main/java/org/opendaylight/yangtools/yang/ir/StatementOutputV1.java b/parser/yang-ir/src/main/java/org/opendaylight/yangtools/yang/ir/StatementOutputV1.java
new file mode 100644 (file)
index 0000000..88671a0
--- /dev/null
@@ -0,0 +1,233 @@
+/*
+ * Copyright (c) 2022 PANTHEON.tech, s.r.o. and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.yangtools.yang.ir;
+
+import java.io.DataOutput;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.opendaylight.yangtools.yang.ir.IRArgument.Concatenation;
+import org.opendaylight.yangtools.yang.ir.IRArgument.Single;
+import org.opendaylight.yangtools.yang.ir.IRKeyword.Qualified;
+
+final class StatementOutputV1 extends StatementOutput {
+    private final Map<IRKeyword, Integer> keywords = new HashMap<>();
+    private final Map<String, Integer> strings = new HashMap<>();
+
+    StatementOutputV1(final DataOutput out) {
+        super(out);
+    }
+
+    @Override
+    void writeStatement(final IRStatement stmt) throws IOException {
+        final List<? extends IRStatement> statements = stmt.statements();
+        final int size = statements.size();
+        final int sizeBits;
+        if (size == 0) {
+            sizeBits = IOConstantsV1.HDR_SIZE_0;
+        } else if (size <= 255) {
+            sizeBits = IOConstantsV1.HDR_SIZE_U8;
+        } else if (size <= 65535) {
+            sizeBits = IOConstantsV1.HDR_SIZE_U16;
+        } else {
+            sizeBits = IOConstantsV1.HDR_SIZE_S32;
+        }
+
+        final IRKeyword keyword = stmt.keyword();
+        final Integer keyCode = keywords.get(keyword);
+        final int keyBits;
+        if (keyCode != null) {
+            final int key = keyCode;
+            if (key <= 255) {
+                keyBits = IOConstantsV1.HDR_KEY_REF_U8;
+            } else if (size <= 65535) {
+                keyBits = IOConstantsV1.HDR_KEY_REF_U16;
+            } else {
+                keyBits = IOConstantsV1.HDR_KEY_REF_S32;
+            }
+        } else {
+            keyBits = keyword instanceof Qualified ? IOConstantsV1.HDR_KEY_DEF_QUAL : IOConstantsV1.HDR_KEY_DEF_UQUAL;
+        }
+
+        final IRArgument argument = stmt.argument();
+        if (stmt instanceof IRStatement.Z22) {
+            writeHeader(keyBits, IOConstantsV1.HDR_LOCATION_22, sizeBits, argument);
+            out.writeShort(stmt.startLine());
+            out.writeShort(stmt.startColumn());
+        } else if (stmt instanceof IRStatement.Z31 z31) {
+            writeHeader(keyBits, IOConstantsV1.HDR_LOCATION_31, sizeBits, argument);
+            out.writeInt(z31.value());
+        } else {
+            writeHeader(keyBits, IOConstantsV1.HDR_LOCATION_44, sizeBits, argument);
+            out.writeInt(stmt.startLine());
+            out.writeInt(stmt.startColumn());
+        }
+
+        switch (keyBits) {
+            case IOConstantsV1.HDR_KEY_REF_U8:
+                out.writeByte(keyCode);
+                break;
+            case IOConstantsV1.HDR_KEY_REF_U16:
+                out.writeShort(keyCode);
+                break;
+            case IOConstantsV1.HDR_KEY_REF_S32:
+                out.writeInt(keyCode);
+                break;
+            case IOConstantsV1.HDR_KEY_DEF_QUAL:
+                writeString(keyword.prefix());
+                writeString(keyword.identifier());
+                keywords.put(keyword, keywords.size());
+                break;
+            case IOConstantsV1.HDR_KEY_DEF_UQUAL:
+                writeString(keyword.identifier());
+                keywords.put(keyword, keywords.size());
+                break;
+            default:
+                throw new IllegalStateException("Unhandled key bits " + keyBits);
+        }
+
+        if (argument != null) {
+            writeArgument(argument);
+        }
+
+        switch (sizeBits) {
+            case IOConstantsV1.HDR_SIZE_0:
+                // All done
+                return;
+            case IOConstantsV1.HDR_SIZE_U8:
+                out.writeByte(statements.size());
+                break;
+            case IOConstantsV1.HDR_SIZE_U16:
+                out.writeShort(size);
+                break;
+            case IOConstantsV1.HDR_SIZE_S32:
+                out.writeInt(size);
+                break;
+            default:
+                throw new IllegalStateException("Unhandled size bits " + sizeBits);
+        }
+
+        for (IRStatement child : statements) {
+            writeStatement(child);
+        }
+    }
+
+    private void writeString(final String str) throws IOException {
+        final Integer key = strings.get(str);
+        if (key != null) {
+            writeStringRef(key);
+        } else {
+            writeStringDef(0, str);
+        }
+    }
+
+    private void writeStringDef(final int bits, final String str) throws IOException {
+        strings.put(str, strings.size());
+
+        final int length = str.length();
+        if (length <= Short.MAX_VALUE / 2) {
+            out.writeByte(IOConstantsV1.STR_DEF_UTF | bits);
+            out.writeUTF(str);
+        } else if (length <= 1048576) {
+            final byte[] bytes = str.getBytes(StandardCharsets.UTF_8);
+            if (bytes.length < 65536) {
+                out.writeByte(IOConstantsV1.STR_DEF_U16 | bits);
+                out.writeShort(bytes.length);
+            } else {
+                out.writeByte(IOConstantsV1.STR_DEF_S32 | bits);
+                out.writeInt(bytes.length);
+            }
+            out.write(bytes);
+        } else {
+            out.writeByte(IOConstantsV1.STR_DEF_CHARS | bits);
+            out.writeInt(length);
+            out.writeChars(str);
+        }
+    }
+
+    private void writeStringRef(final int strCode) throws IOException {
+        if (strCode <= 255) {
+            out.writeByte(IOConstantsV1.STR_REF_U8);
+            out.writeByte(strCode);
+        } else if (strCode <= 65535) {
+            out.writeByte(IOConstantsV1.STR_REF_U16);
+            out.writeShort(strCode);
+        } else {
+            out.writeByte(IOConstantsV1.STR_REF_S32);
+            out.writeInt(strCode);
+        }
+    }
+
+    private void writeHeader(final int keyBits, final int locationBits, final int sizeBits, final IRArgument argument)
+            throws IOException {
+        final int argBits = argument != null ? IOConstantsV1.HDR_ARGUMENT_PRESENT : IOConstantsV1.HDR_ARGUMENT_ABSENT;
+        out.writeByte(keyBits | sizeBits | argBits | locationBits);
+    }
+
+    private void writeArgument(final IRArgument argument) throws IOException {
+        if (argument instanceof Single) {
+            writeArgument((Single) argument);
+        } else if (argument instanceof Concatenation) {
+            writeArgument((Concatenation) argument);
+        } else {
+            throw new IllegalStateException("Unhandled argument " + argument);
+        }
+    }
+
+    private void writeArgument(final Single argument) throws IOException {
+        final int type;
+        if (argument.isValidIdentifier()) {
+            type = IOConstantsV1.ARG_TYPE_IDENTIFIER;
+        } else if (argument.needQuoteCheck()) {
+            type = IOConstantsV1.ARG_TYPE_UQUOT;
+        } else if (argument.needUnescape()) {
+            type = IOConstantsV1.ARG_TYPE_DQUOT;
+        } else {
+            type = IOConstantsV1.ARG_TYPE_SQUOT;
+        }
+
+        final String str = argument.string();
+        final Integer existing = strings.get(str);
+        if (existing != null) {
+            final int strCode = existing;
+            if (strCode <= 255) {
+                out.writeByte(type | IOConstantsV1.STR_REF_U8);
+                out.writeByte(strCode);
+            } else if (strCode <= 65535) {
+                out.writeByte(type | IOConstantsV1.STR_REF_U16);
+                out.writeShort(strCode);
+            } else {
+                out.writeByte(type | IOConstantsV1.STR_REF_S32);
+                out.writeInt(strCode);
+            }
+        } else {
+            writeStringDef(type, str);
+        }
+    }
+
+    private void writeArgument(final Concatenation argument) throws IOException {
+        final List<? extends Single> parts = argument.parts();
+        final int size = parts.size();
+        if (size <= 255) {
+            out.writeByte(IOConstantsV1.ARG_TYPE_CONCAT_U8);
+            out.writeByte(size);
+        } else if (size <= 65535) {
+            out.writeByte(IOConstantsV1.ARG_TYPE_CONCAT_U16);
+            out.writeShort(size);
+        } else {
+            out.writeByte(IOConstantsV1.ARG_TYPE_CONCAT_S32);
+            out.writeInt(size);
+        }
+
+        for (Single part : parts) {
+            writeArgument(part);
+        }
+    }
+}
diff --git a/parser/yang-parser-rfc7950/src/test/java/org/opendaylight/yangtools/yang/parser/rfc7950/antlr/IOSupportTest.java b/parser/yang-parser-rfc7950/src/test/java/org/opendaylight/yangtools/yang/parser/rfc7950/antlr/IOSupportTest.java
new file mode 100644 (file)
index 0000000..bd2ef7e
--- /dev/null
@@ -0,0 +1,62 @@
+/*
+ * Copyright (c) 2022 PANTHEON.tech, s.r.o. and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.yangtools.yang.parser.rfc7950.antlr;
+
+import static org.junit.Assert.assertEquals;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.opendaylight.yangtools.yang.ir.IOSupport;
+import org.opendaylight.yangtools.yang.ir.IRStatement;
+import org.opendaylight.yangtools.yang.model.repo.api.YangIRSchemaSource;
+import org.opendaylight.yangtools.yang.parser.rfc7950.repo.TextToIRTransformer;
+import org.opendaylight.yangtools.yang.stmt.TestUtils;
+
+class IOSupportTest {
+    private static YangIRSchemaSource FOO;
+
+    @BeforeAll
+    static void beforeClass() throws Exception {
+        FOO = TextToIRTransformer.transformText(TestUtils.assertSchemaSource("/bugs/YT1089/foo.yang"));
+    }
+
+    @Test
+    void testSerializedSize() throws IOException {
+        final byte[] bytes = serialize(FOO.getRootStatement());
+        assertEquals(485, bytes.length);
+    }
+
+    @Test
+    void testSerdes() throws IOException {
+        final var orig = FOO.getRootStatement();
+        assertEquals(orig, deserialize(serialize(orig)));
+    }
+
+    private static byte[] serialize(final IRStatement stmt) throws IOException {
+        final var baos = new ByteArrayOutputStream();
+
+        try (var dos = new DataOutputStream(baos)) {
+            IOSupport.writeStatement(dos, stmt);
+        }
+
+        return baos.toByteArray();
+    }
+
+    private static IRStatement deserialize(final byte[] bytes) throws IOException {
+        try (var dis = new DataInputStream(new ByteArrayInputStream(bytes))) {
+            final var stmt = IOSupport.readStatement(dis);
+            assertEquals(0, dis.available());
+            return stmt;
+        }
+    }
+}
index 9206be9c89e1a89af0df207bbb37c8338eecc07a..29ddfcd4e184c07ab8c9442b068fa2ce84de0d2e 100644 (file)
@@ -10,6 +10,7 @@ package org.opendaylight.yangtools.yang.stmt;
 import java.io.File;
 import java.io.IOException;
 import java.net.URI;
+import java.net.URISyntaxException;
 import java.nio.file.Path;
 import java.util.ArrayList;
 import java.util.Collection;
@@ -87,8 +88,7 @@ public final class TestUtils {
             final @Nullable Set<QName> supportedFeatures) throws Exception {
         final var reactor = RFC7950Reactors.defaultReactor().newBuild();
         for (var resourcePath : yangSourceFilePath) {
-            reactor.addSource(YangStatementStreamSource.create(YangTextSchemaSource.forPath(Path.of(
-                TestUtils.class.getResource(resourcePath).toURI()))));
+            reactor.addSource(YangStatementStreamSource.create(assertSchemaSource(resourcePath)));
         }
         if (supportedFeatures != null) {
             reactor.setSupportedFeatures(FeatureSet.of(supportedFeatures));
@@ -96,6 +96,14 @@ public final class TestUtils {
         return reactor.buildEffective();
     }
 
+    public static YangTextSchemaSource assertSchemaSource(final String resourcePath) {
+        try {
+            return YangTextSchemaSource.forPath(Path.of(TestUtils.class.getResource(resourcePath).toURI()));
+        } catch (URISyntaxException e) {
+            throw new AssertionError(e);
+        }
+    }
+
     // FIXME: these remain unaudited
 
     public static EffectiveModelContext loadYinModules(final URI resourceDirectory)