diff --git a/docs/serialization.md b/docs/serialization.md index 2ede928a02..2fd8958198 100644 --- a/docs/serialization.md +++ b/docs/serialization.md @@ -124,8 +124,14 @@ The header is structured like the following table: | varuint | content pool size | After the header comes the body of the serialized string. -The body consists of a sequence of nodes that is built using a prefix traversal order of the syntax tree. -Each node is structured like the following table: +The body consists of a sequence of nodes that is built using a postfix traversal order of the syntax tree: the children of each node come before the node itself, so the root node comes last. +The body ends at the content pool offset specified in the header. + +The children of a node are the values of its `node`, `node?`, and `node[]` fields, which are serialized in the order in which the fields are listed in `config.yml`. +A `node` field is serialized as its node, a `node?` field is serialized as its node or as a single `0` byte if the node is not present, and a `node[]` field is serialized as each of its nodes in order. +A loader can therefore keep a stack of loaded nodes: a `0` byte pushes an absent node, and each node pops its children off the top of the stack (they are in field order) and then pushes itself. + +After its children, each node is structured like the following table: | # bytes | field | | --- | --- | @@ -134,12 +140,11 @@ Each node is structured like the following table: | location | node location | | varuint | node flags | -Every field on the node is then appended to the serialized string. The fields can be determined by referencing `config.yml`. Depending on the type of field, it could take a couple of different forms, described below: +Every other field on the node is then appended to the serialized string. The fields can be determined by referencing `config.yml`. Depending on the type of field, it could take a couple of different forms, described below: * `double` - A field that is a `double`. This is structured as a sequence of 8 bytes in native endian order. -* `node` - A field that is a node. This is structured just as like parent node. -* `node?` - A field that is a node that is optionally present. If the node is not present, then a single `0` byte will be written in its place. If it is present, then it will be structured just as like parent node. -* `node[]` - A field that is an array of nodes. This is structured as a variable-length integer length, followed by the child nodes themselves. +* `node`, `node?` - Nothing is appended here, since the node has already been serialized as a child. +* `node[]` - A field that is an array of nodes. This is structured as a variable-length integer length, which is the number of children that belong to this field. * `string` - A field that is a string. For example, this is used as the name of the method in a call node, since it cannot directly reference the source string (as in `@-` or `foo=`). This is structured as a variable-length integer byte length, followed by the string bytes (_without_ a trailing null byte). * `constant` - A variable-length integer that represents an index in the constant pool. * `constant?` - An optional variable-length integer that represents an index in the constant pool. If it's not present, then a single `0` byte will be written in its place. @@ -149,6 +154,14 @@ Every field on the node is then appended to the serialized string. The fields ca * `uint8` - A field that is an 8-bit unsigned integer. This is structured as a single byte. * `uint32` - A field that is a 32-bit unsigned integer. This is structured as a variable-length integer. +The `body` field of a `DefNode` is lazy, which means that loaders can skip it and load it later. The children of a `DefNode` are preceded by the following, which loaders that do not skip lazy fields can ignore: + +| # bytes | field | +| --- | --- | +| `1` | `255` | +| `4` | the byte length of the children that come before the `body` child | +| `4` | the byte length of the `body` child | + After the syntax tree, the content pool is serialized. This is a list of constants that were referenced from within the tree. The content pool begins at the offset specified in the header. Every constant is embedded in the serialization. Each constant is structured as follows: | # bytes | field | diff --git a/include/prism/compiler/branch.h b/include/prism/compiler/branch.h new file mode 100644 index 0000000000..81754713ad --- /dev/null +++ b/include/prism/compiler/branch.h @@ -0,0 +1,26 @@ +/** + * @file compiler/branch.h + */ +#ifndef PRISM_COMPILER_BRANCH_H +#define PRISM_COMPILER_BRANCH_H + +/** + * Branch prediction hints. These tell the compiler which side of a branch to + * predicate the fast path through, so it can lay out instructions to favour + * that side and steer the static branch predictor. + */ +#if defined(__GNUC__) || defined(__clang__) + /** The compiler should predicate that this branch will be taken. */ + #define PRISM_LIKELY(x) __builtin_expect(!!(x), 1) + + /** The compiler should predicate that this branch will not be taken. */ + #define PRISM_UNLIKELY(x) __builtin_expect(!!(x), 0) +#else + /** Void because this platform does not support branch prediction hints. */ + #define PRISM_LIKELY(x) (x) + + /** Void because this platform does not support branch prediction hints. */ + #define PRISM_UNLIKELY(x) (x) +#endif + +#endif diff --git a/javascript/test.js b/javascript/test.js index 3131ec30dd..b7e20920a2 100644 --- a/javascript/test.js +++ b/javascript/test.js @@ -427,3 +427,11 @@ suite("location", () => { assert(node.receiver.location.endCodeUnitsColumn("utf-8") === 5); }); }); + +suite("serialization", () => { + test("deeply nested trees", () => { + // 20,000 nodes deep, which is deeper than a walk of the tree can recurse. + const result = parse("1" + "+1".repeat(20000)); + assert(result.value instanceof nodes.ProgramNode); + }); +}); diff --git a/prism.gemspec b/prism.gemspec index db4b311ca3..c4c3f808bf 100644 --- a/prism.gemspec +++ b/prism.gemspec @@ -49,6 +49,7 @@ Gem::Specification.new do |spec| "include/prism/compiler/accel.h", "include/prism/compiler/align.h", "include/prism/compiler/assume.h", + "include/prism/compiler/branch.h", "include/prism/compiler/exported.h", "include/prism/compiler/fallthrough.h", "include/prism/compiler/filesystem.h", diff --git a/rakelib/serialization.rake b/rakelib/serialization.rake index a4634a2637..8ff3bfae91 100644 --- a/rakelib/serialization.rake +++ b/rakelib/serialization.rake @@ -62,6 +62,14 @@ task "test:java_loader:internal" => :compile do require 'java' require_relative '../tmp/prism.jar' + # A loader that skips the bodies of DefNodes, which are then loaded on demand + # by DefNode#getNonLazy. + class LazyDefNodeLoader < org.ruby_lang.prism.Loader + def loadDefNodesLazily + true + end + end + Dir["**/*.txt", base: fixtures].each do |relative| path = "#{fixtures}/#{relative}" puts @@ -71,15 +79,38 @@ task "test:java_loader:internal" => :compile do puts parse_result.value end + # Check that loading the bodies of DefNodes lazily and then loading them with + # getNonLazy gives the same tree as loading eagerly. + describe = -> (node, force) { + return "nil" if node.nil? + node = node.getNonLazy if force && node.is_a?(org.ruby_lang.prism.Nodes::DefNode) + "#{node.getClass.getSimpleName}@#{node.startOffset}(#{node.childNodes.map { |child| describe.call(child, force) }.join(", ")})" + } + + lazy_failures = [] + Dir["**/*.txt", base: fixtures].each do |relative| + path = "#{fixtures}/#{relative}" + serialized = Prism.dump_file(path).unpack('c*') + + eager = org.ruby_lang.prism.Loader.load(serialized).value + lazy = LazyDefNodeLoader.new(serialized.to_java(:byte)).java_send(:load).value + lazy_failures << path if describe.call(eager, false) != describe.call(lazy, true) + end + + abort "Lazily loaded DefNodes differ from eagerly loaded ones in:\n#{lazy_failures.join("\n")}" unless lazy_failures.empty? + # Check that MarkNewlinesVisitor.java (run by Loader.load() above) marks # exactly the same lines as Ruby's newlines.rb, using the fixtures generated # by the test:java_loader:newline_fixtures task. newline_fixtures = File.expand_path("../java/newline_fixtures.txt", __dir__) raise "no #{newline_fixtures}, run the test:java_loader task to generate it" unless File.exist?(newline_fixtures) + # A lazy DefNode carries its own newline flag, while the DefNode returned by + # getNonLazy carries the newline flags of its descendants. collect_newlines = -> (node, source, lines) { return if node.nil? lines << source.line(node.startOffset) if node.hasNewLineFlag + node = node.getNonLazy if node.is_a?(org.ruby_lang.prism.Nodes::DefNode) node.childNodes.each { |child| collect_newlines.call(child, source, lines) } } @@ -88,16 +119,21 @@ task "test:java_loader:internal" => :compile do File.readlines(newline_fixtures, chomp: true).each do |fixture| path, expected = fixture.split("\t", 2) - serialized = Prism.dump_file(path) - parse_result = org.ruby_lang.prism.Loader.load(serialized.unpack('c*')) - - lines = [] - collect_newlines.call(parse_result.value, parse_result.source, lines) - actual = lines.sort.join(" ") + serialized = Prism.dump_file(path).unpack('c*') + parse_results = { + "java" => org.ruby_lang.prism.Loader.load(serialized), + "java (lazy)" => LazyDefNodeLoader.new(serialized.to_java(:byte)).java_send(:load) + } checked += 1 - if actual != (expected || "") - failures << "#{path}\n ruby: #{expected}\n java: #{actual}" + parse_results.each do |name, parse_result| + lines = [] + collect_newlines.call(parse_result.value, parse_result.source, lines) + actual = lines.sort.join(" ") + + if actual != (expected || "") + failures << "#{path}\n ruby: #{expected}\n #{name}: #{actual}" + end end end diff --git a/rbi/generated/prism/serialize.rbi b/rbi/generated/prism/serialize.rbi index 8e693c3f2a..ce22a82f37 100644 --- a/rbi/generated/prism/serialize.rbi +++ b/rbi/generated/prism/serialize.rbi @@ -138,9 +138,6 @@ module Prism sig { returns(Integer) } def load_uint32; end - sig { params(constant_pool: ConstantPool, encoding: Encoding, freeze: T::Boolean).returns(::T.nilable(Node)) } - def load_optional_node(constant_pool, encoding, freeze); end - sig { params(encoding: Encoding).returns(String) } def load_string(encoding); end @@ -168,8 +165,12 @@ module Prism sig { params(constant_pool: ConstantPool, encoding: Encoding).returns(::T.nilable(Symbol)) } def load_optional_constant(constant_pool, encoding); end - sig { params(constant_pool: ConstantPool, encoding: Encoding, freeze: T::Boolean).returns(Node) } - def load_node(constant_pool, encoding, freeze); end + # Load the nodes that are serialized before the given offset and return + # the root. Nodes are serialized in post-order, so the children of each + # node are the topmost values on the stack, in field order, by the time + # its record is read. + sig { params(constant_pool: ConstantPool, encoding: Encoding, freeze: T::Boolean, finish: Integer).returns(Node) } + def load_nodes(constant_pool, encoding, freeze, finish); end sig { void } def define_load_node_lambdas; end diff --git a/sig/generated/prism/serialize.rbs b/sig/generated/prism/serialize.rbs index e1eb2cedbb..79a43cd5d7 100644 --- a/sig/generated/prism/serialize.rbs +++ b/sig/generated/prism/serialize.rbs @@ -149,9 +149,6 @@ module Prism # : () -> Integer def load_uint32: () -> Integer - # : (ConstantPool constant_pool, Encoding encoding, bool freeze) -> node? - def load_optional_node: (ConstantPool constant_pool, Encoding encoding, bool freeze) -> node? - # : (Encoding encoding) -> String def load_string: (Encoding encoding) -> String @@ -181,8 +178,13 @@ module Prism # : (ConstantPool constant_pool, Encoding encoding) -> Symbol? def load_optional_constant: (ConstantPool constant_pool, Encoding encoding) -> Symbol? - # : (ConstantPool constant_pool, Encoding encoding, bool freeze) -> node - def load_node: (ConstantPool constant_pool, Encoding encoding, bool freeze) -> node + # Load the nodes that are serialized before the given offset and return + # the root. Nodes are serialized in post-order, so the children of each + # node are the topmost values on the stack, in field order, by the time + # its record is read. + # + # : (ConstantPool constant_pool, Encoding encoding, bool freeze, Integer finish) -> node + def load_nodes: (ConstantPool constant_pool, Encoding encoding, bool freeze, Integer finish) -> node @load_node_lambdas: Array[Proc] diff --git a/src/prism.c b/src/prism.c index 262c238fcb..b8cbdacf83 100644 --- a/src/prism.c +++ b/src/prism.c @@ -1,4 +1,5 @@ #include "prism/compiler/accel.h" +#include "prism/compiler/branch.h" #include "prism/compiler/fallthrough.h" #include "prism/compiler/unused.h" @@ -68,24 +69,6 @@ # define PM_STATIC_ASSERT(line, condition, message) typedef char PM_CONCATENATE(static_assert_, line)[(condition) ? 1 : -1] #endif -/** - * Support PRISM_LIKELY and PRISM_UNLIKELY to help the compiler optimize its - * branch predication. - */ -#if defined(__GNUC__) || defined(__clang__) - /** The compiler should predicate that this branch will be taken. */ - #define PRISM_LIKELY(x) __builtin_expect(!!(x), 1) - - /** The compiler should predicate that this branch will not be taken. */ - #define PRISM_UNLIKELY(x) __builtin_expect(!!(x), 0) -#else - /** Void because this platform does not support branch prediction hints. */ - #define PRISM_LIKELY(x) (x) - - /** Void because this platform does not support branch prediction hints. */ - #define PRISM_UNLIKELY(x) (x) -#endif - /** * The prism version and the serialization format. */ diff --git a/templates/java/api/src/main/java-templates/org/ruby_lang/prism/Loader.java.erb b/templates/java/api/src/main/java-templates/org/ruby_lang/prism/Loader.java.erb index f1785e5d30..76ed2a5763 100644 --- a/templates/java/api/src/main/java-templates/org/ruby_lang/prism/Loader.java.erb +++ b/templates/java/api/src/main/java-templates/org/ruby_lang/prism/Loader.java.erb @@ -27,6 +27,15 @@ public class Loader { <%- end -%> } + /** + * Whether to skip the bodies of DefNodes while loading. A lazily loaded + * DefNode has a null body until it is replaced with DefNode#getNonLazy(), + * which loads the whole DefNode again from the serialized output. + */ + protected boolean loadDefNodesLazily() { + return false; + } + private static final class ConstantPool { private final Loader loader; @@ -108,7 +117,7 @@ public class Loader { Nodes.Node node; if (errors.length == 0) { - node = loadNode(); + node = loadNodes(constantPoolBufferOffset, -1); int left = constantPoolBufferOffset - buffer.position(); if (left != 0) { @@ -193,15 +202,6 @@ public class Loader { return warnings; } - private Nodes.Node loadOptionalNode() { - if (buffer.get(buffer.position()) != 0) { - return loadNode(); - } else { - buffer.position(buffer.position() + 1); // continue after the 0 byte - return null; - } - } - private <%= id_type %> loadConstant() { return constantPool.get(buffer, loadVarUInt()); } @@ -323,86 +323,112 @@ public class Loader { return negative ? result.negate() : result; } - <%- - base_params = [*("nodeId" if Prism::Template::INCLUDE_NODE_ID), "startOffset", "length"] - base_params_sig = base_params.map { "int #{_1}" }.join(", ") - -%> - private Nodes.Node loadNode() { - int type = buffer.get() & 0xFF; - <%- if Prism::Template::INCLUDE_NODE_ID -%> - int nodeId = loadVarUInt(); - <%- end -%> - int startOffset = loadVarUInt(); - int length = loadVarUInt(); + private static final int LAZY_MARKER = <%= Prism::Template::LAZY_MARKER %>; - switch (type) { - <%- array_types = [] -%> - <%- nodes.each_with_index do |node, index| -%> - case <%= index + 1 %>: - <%- - params = [] - params << "nodeId" if Prism::Template::INCLUDE_NODE_ID - params << "startOffset" << "length" - params << "buffer.getInt()" << "null" if node.needs_serialized_length? - params << "loadFlags()" if node.flags - params.concat node.semantic_fields.map { |field| - case field - when Prism::Template::NodeField then "#{field.java_cast}loadNode()" - when Prism::Template::OptionalNodeField then "#{field.java_cast}loadOptionalNode()" - when Prism::Template::StringField then "loadString()" - when Prism::Template::NodeListField then - element_type = field.java_type.sub('[]', '') - array_types << element_type - "load#{element_type}s()" - when Prism::Template::ConstantField then "loadConstant()" - when Prism::Template::OptionalConstantField then "loadOptionalConstant()" - when Prism::Template::ConstantListField then "loadConstants()" - when Prism::Template::LocationField then "loadLocation()" - when Prism::Template::OptionalLocationField then "loadOptionalLocation()" - when Prism::Template::UInt8Field then "buffer.get()" - when Prism::Template::UInt32Field then "loadVarUInt()" - when Prism::Template::IntegerField then "loadInteger()" - when Prism::Template::DoubleField then "buffer.getDouble()" - else raise - end + /** + * The stack of nodes that have been loaded but not yet attached to their + * parent, along with the lazy subtrees that are pending. + */ + private static final class NodeStack { + + private Nodes.Node[] nodes = new Nodes.Node[64]; + private int size = 0; + + /** For each pending lazy subtree, its marker position, start, and end. */ + private int[] lazy = new int[3 * 8]; + private int lazySize = 0; + + void push(Nodes.Node node) { + if (size == nodes.length) { + nodes = java.util.Arrays.copyOf(nodes, size * 2); } - $DefNode_params = params if node.name == "DefNode" - -%> - <%- if node.name == "DefNode" -%> - return loadDefNode(<%= base_params.join(", ") -%>); - <%- else -%> - return new Nodes.<%= node.name %>(<%= params.join(", ") -%>); - <%- end -%> - <%- end -%> - default: - throw new Error("Unknown node type: " + type); + nodes[size++] = node; } - } - // Can be overridden to use createLazyDefNode instead - protected Nodes.DefNode loadDefNode(<%= base_params_sig -%>) { - return createDefNode(<%= base_params.join(", ") -%>); - } + Nodes.Node pop() { + Nodes.Node node = nodes[--size]; + nodes[size] = null; + return node; + } + + void pushLazy(int marker, int start, int end) { + if (lazySize * 3 == lazy.length) { + lazy = java.util.Arrays.copyOf(lazy, lazy.length * 2); + } + lazy[lazySize * 3] = marker; + lazy[lazySize * 3 + 1] = start; + lazy[lazySize * 3 + 2] = end; + lazySize++; + } - protected Nodes.DefNode createLazyDefNode(<%= base_params_sig -%>) { - int bufferPosition = buffer.position(); - int serializedLength = buffer.getInt(); - // Load everything except the body and locals, because the name, receiver, parameters are still needed for lazily defining the method - Nodes.DefNode lazyDefNode = new Nodes.DefNode(<%= base_params.join(", ") -%>, -bufferPosition, this, loadConstant(), loadOptionalNode(), (Nodes.ParametersNode) loadOptionalNode(), null, Nodes.EMPTY_IDENTIFIER_ARRAY); - buffer.position(bufferPosition + serializedLength); // skip past the serialized DefNode - return lazyDefNode; + /** + * Returns the marker position of the most recently pushed lazy + * subtree, or -1 if there is none. That subtree always belongs to the + * node whose record is being loaded, because the lazy subtrees of its + * descendants are popped when their own records are loaded. + */ + int popLazy() { + return lazySize == 0 ? -1 : lazy[--lazySize * 3]; + } } - protected Nodes.DefNode createDefNode(<%= base_params_sig -%>) { - return new Nodes.DefNode(<%= $DefNode_params.join(", ") -%>); + /** + * Loads the nodes serialized from the current position, stopping at the + * given offset once a single root remains, and returns that root. Nodes + * are serialized in post-order, so the children of each node are the + * topmost values on the stack, in field order, by the time its record is + * loaded. The lazy marker at eagerMarker, if any, is loaded eagerly even + * when DefNodes are loaded lazily. + */ + private Nodes.Node loadNodes(int end, int eagerMarker) { + boolean lazy = loadDefNodesLazily(); + NodeStack stack = new NodeStack(); + + while (buffer.position() < end || stack.size != 1) { + if (stack.lazySize > 0 && buffer.position() == stack.lazy[stack.lazySize * 3 - 2]) { + buffer.position(stack.lazy[stack.lazySize * 3 - 1]); + continue; + } + + int type = buffer.get() & 0xFF; + switch (type) { + case 0: + stack.push(null); + break; + case LAZY_MARKER: { + int marker = buffer.position() - 1; + int skip = buffer.getInt(); + int length = buffer.getInt(); + if (lazy && marker != eagerMarker) { + int start = buffer.position() + skip; + stack.pushLazy(marker, start, start + length); + } + break; + } + <%- nodes.each.with_index(1) do |node, index| -%> + case <%= index %>: + stack.push(load<%= node.name %>(stack)); + break; + <%- end -%> + default: + throw new Error("Unknown node type: " + type); + } + } + + return stack.pop(); } - Nodes.DefNode createDefNodeFromSavedPosition(<%= base_params_sig -%>, int bufferPosition) { + Nodes.DefNode loadNonLazyDefNode(int lazyPosition) { Nodes.DefNode node; // This method mutates the buffer position and may be called from different threads so we must synchronize synchronized (this) { - buffer.position(bufferPosition); - node = createDefNode(<%= base_params.join(", ") -%>); + buffer.position(lazyPosition + 1); + int skip = buffer.getInt(); + int length = buffer.getInt(); + int end = buffer.position() + skip + length; + + buffer.position(lazyPosition); + node = (Nodes.DefNode) loadNodes(end, lazyPosition); } MarkNewlinesVisitor visitor = new MarkNewlinesVisitor(source); @@ -410,21 +436,82 @@ public class Loader { return node; } + <%- array_types = [] -%> + <%- nodes.each do |node| -%> + + private Nodes.<%= node.name %> load<%= node.name %>(NodeStack stack) { + <%- if Prism::Template::INCLUDE_NODE_ID -%> + int nodeId = loadVarUInt(); + <%- end -%> + int startOffset = loadVarUInt(); + int length = loadVarUInt(); + <%- if node.flags -%> + short flags = loadFlags(); + <%- end -%> + <%- node.semantic_fields.each do |field| -%> + <%- case field -%> + <%- when Prism::Template::NodeField, Prism::Template::OptionalNodeField -%> + <%- when Prism::Template::NodeListField -%> + int <%= field.name %>Size = loadVarUInt(); + <%- else -%> + var field_<%= field.name %> = <%= + case field + when Prism::Template::StringField then "loadString()" + when Prism::Template::ConstantField then "loadConstant()" + when Prism::Template::OptionalConstantField then "loadOptionalConstant()" + when Prism::Template::ConstantListField then "loadConstants()" + when Prism::Template::LocationField then "loadLocation()" + when Prism::Template::OptionalLocationField then "loadOptionalLocation()" + when Prism::Template::UInt8Field then "buffer.get()" + when Prism::Template::UInt32Field then "loadVarUInt()" + when Prism::Template::IntegerField then "loadInteger()" + when Prism::Template::DoubleField then "buffer.getDouble()" + else raise + end + %>; + <%- end -%> + <%- end -%> + <%- if node.lazy_field -%> + int lazyPosition = stack.popLazy(); + <%- end -%> + <%- node.child_fields.reverse_each do |field| -%> + <%- case field -%> + <%- when Prism::Template::NodeListField -%> + <%- element_type = field.java_type.sub("[]", ""); array_types << element_type -%> + var field_<%= field.name %> = pop<%= element_type %>s(stack, <%= field.name %>Size); + <%- else -%> + <%- if field == node.lazy_field -%> + var field_<%= field.name %> = lazyPosition == -1 ? <%= field.java_cast %>stack.pop() : null; + <%- else -%> + var field_<%= field.name %> = <%= field.java_cast %>stack.pop(); + <%- end -%> + <%- end -%> + <%- end -%> + <%- + params = [] + params << "nodeId" if Prism::Template::INCLUDE_NODE_ID + params << "startOffset" << "length" + params << "lazyPosition" << "lazyPosition == -1 ? null : this" if node.lazy_field + params << "flags" if node.flags + params.concat(node.semantic_fields.map { |field| "field_#{field.name}" }) + -%> + return new Nodes.<%= node.name %>(<%= params.join(", ") %>); + } + <%- end -%> <%- array_types.uniq.each do |type| -%> private static final Nodes.<%= type %>[] EMPTY_<%= type %>_ARRAY = {}; - private Nodes.<%= type %>[] load<%= type %>s() { - int length = loadVarUInt(); + private static Nodes.<%= type %>[] pop<%= type %>s(NodeStack stack, int length) { if (length == 0) { return EMPTY_<%= type %>_ARRAY; } Nodes.<%= type %>[] nodes = new Nodes.<%= type %>[length]; - for (int i = 0; i < length; i++) { - <%- if type == 'Node' -%> - nodes[i] = loadNode(); + for (int i = length - 1; i >= 0; i--) { + <%- if type == "Node" -%> + nodes[i] = stack.pop(); <%- else -%> - nodes[i] = (Nodes.<%= type %>) loadNode(); + nodes[i] = (Nodes.<%= type %>) stack.pop(); <%- end -%> } return nodes; diff --git a/templates/java/api/src/main/java-templates/org/ruby_lang/prism/Nodes.java.erb b/templates/java/api/src/main/java-templates/org/ruby_lang/prism/Nodes.java.erb index bcfe47f7c6..7f72ddff7e 100644 --- a/templates/java/api/src/main/java-templates/org/ruby_lang/prism/Nodes.java.erb +++ b/templates/java/api/src/main/java-templates/org/ruby_lang/prism/Nodes.java.erb @@ -217,8 +217,10 @@ public abstract class Nodes { * */ public static final class <%= node.name -%> extends Node { - <%- if node.needs_serialized_length? -%> - public final int serializedLength; + <%- if node.lazy_field -%> + /** The position of the lazy marker in the serialized output, if this node is lazy. */ + public final int lazyPosition; + /** The loader that loaded this node, if this node is lazy. */ public final Loader loader; <%- end -%> <%- if node.flags -%> @@ -247,10 +249,10 @@ public abstract class Nodes { params = [] params << "int nodeId" if Prism::Template::INCLUDE_NODE_ID params << "int startOffset" << "int length" - if node.needs_serialized_length? - params << "int serializedLength" + if node.lazy_field + params << "int lazyPosition" params << "Loader loader" - end + end params << "short flags" if node.flags params.concat(node.semantic_fields.map { |field| "#{field.java_type} #{field.name}" }) -%> @@ -260,8 +262,8 @@ public abstract class Nodes { <%- else -%> super(startOffset, length); <%- end -%> - <%- if node.needs_serialized_length? -%> - this.serializedLength = serializedLength; + <%- if node.lazy_field -%> + this.lazyPosition = lazyPosition; this.loader = loader; <%- end -%> <%- if node.flags -%> @@ -272,15 +274,15 @@ public abstract class Nodes { <%- end -%> } <%-# extra methods for DefNode -%> - <%- if node.needs_serialized_length? -%> + <%- if node.lazy_field -%> public boolean isLazy() { - return serializedLength < 0; + return loader != null; } public <%= node.name -%> getNonLazy() { if (isLazy()) { - return loader.createDefNodeFromSavedPosition(<%= "nodeId, " if Prism::Template::INCLUDE_NODE_ID %>startOffset, length, -serializedLength); + return loader.loadNonLazy<%= node.name %>(lazyPosition); } else { return this; } diff --git a/templates/javascript/src/deserialize.js.erb b/templates/javascript/src/deserialize.js.erb index e00e0f26a9..edf539dadd 100644 --- a/templates/javascript/src/deserialize.js.erb +++ b/templates/javascript/src/deserialize.js.erb @@ -333,50 +333,76 @@ export function deserialize(source, array) { const constantPoolOffset = buffer.readUint32(); const constants = Array.from({ length: buffer.readVarInt() }, () => null); - return new ParseResult(readRequiredNode(), comments, magicComments, dataLoc, errors, warnings, continuable); - - function readRequiredNode() { - const type = buffer.readByte(); - const nodeID = buffer.readVarInt(); - const location = buffer.readLocation(); - let flags; - - switch (type) { - <%- nodes.each.with_index(1) do |node, index| -%> - case <%= index %>: - <%- if node.needs_serialized_length? -%> - buffer.readUint32(); + return new ParseResult(readNodes(constantPoolOffset), comments, magicComments, dataLoc, errors, warnings, continuable); + + /* Read the nodes that are serialized before the given offset and return the + * root. Nodes are serialized in post-order, so the children of each node are + * the topmost values on the stack, in field order, by the time its record is + * read. */ + function readNodes(finish) { + const stack = []; + + while (buffer.index < finish) { + const type = buffer.readByte(); + + switch (type) { + case 0: + stack.push(null); + break; + case <%= Prism::Template::LAZY_MARKER %>: + buffer.readUint32(); + buffer.readUint32(); + break; + <%- local = ->(field) { field.name == "arguments" ? "arguments_" : field.name.gsub(/_([a-z])/) { $1.upcase } } -%> + <%- nodes.each.with_index(1) do |node, index| -%> + case <%= index %>: { + const nodeID = buffer.readVarInt(); + const location = buffer.readLocation(); + const flags = buffer.readVarInt(); + <%- node.fields.each do |field| -%> + <%- case field -%> + <%- when Prism::Template::NodeField, Prism::Template::OptionalNodeField -%> + <%- when Prism::Template::NodeListField -%> + const <%= local[field] %>Size = buffer.readVarInt(); + <%- else -%> + const <%= local[field] %> = <%= + case field + when Prism::Template::StringField then "buffer.readStringField(flags)" + when Prism::Template::ConstantField then "readRequiredConstant()" + when Prism::Template::OptionalConstantField then "readOptionalConstant()" + when Prism::Template::ConstantListField then "Array.from({ length: buffer.readVarInt() }, readRequiredConstant)" + when Prism::Template::LocationField then "buffer.readLocation()" + when Prism::Template::OptionalLocationField then "buffer.readOptionalLocation()" + when Prism::Template::UInt8Field then "buffer.readByte()" + when Prism::Template::UInt32Field then "buffer.readVarInt()" + when Prism::Template::IntegerField then "readInteger()" + when Prism::Template::DoubleField then "buffer.readDouble()" + else raise + end + %>; + <%- end -%> + <%- end -%> + <%- node.child_fields.reverse_each do |field| -%> + <%- if field.is_a?(Prism::Template::NodeListField) -%> + const <%= local[field] %> = stack.splice(stack.length - <%= local[field] %>Size, <%= local[field] %>Size); + <%- else -%> + const <%= local[field] %> = stack.pop(); + <%- end -%> + <%- end -%> + stack.push(new nodes.<%= node.name %>(<%= ["nodeID", "location", "flags", *node.fields.map { |field| local[field] }].join(", ") %>)); + break; + } <%- end -%> - return new nodes.<%= node.name %>(<%= ["nodeID", "location", "flags = buffer.readVarInt()", *node.fields.map { |field| - case field - when Prism::Template::NodeField then "readRequiredNode()" - when Prism::Template::OptionalNodeField then "readOptionalNode()" - when Prism::Template::StringField then "buffer.readStringField(flags)" - when Prism::Template::NodeListField then "Array.from({ length: buffer.readVarInt() }, readRequiredNode)" - when Prism::Template::ConstantField then "readRequiredConstant()" - when Prism::Template::OptionalConstantField then "readOptionalConstant()" - when Prism::Template::ConstantListField then "Array.from({ length: buffer.readVarInt() }, readRequiredConstant)" - when Prism::Template::LocationField then "buffer.readLocation()" - when Prism::Template::OptionalLocationField then "buffer.readOptionalLocation()" - when Prism::Template::UInt8Field then "buffer.readByte()" - when Prism::Template::UInt32Field then "buffer.readVarInt()" - when Prism::Template::IntegerField then "readInteger()" - when Prism::Template::DoubleField then "buffer.readDouble()" - end - }].join(", ") -%>); - <%- end -%> - default: - throw new Error(`Unknown node type: ${type}`); + default: + throw new Error(`Unknown node type: ${type}`); + } } - } - function readOptionalNode() { - if (buffer.readByte() != 0) { - buffer.index -= 1; - return readRequiredNode(); - } else { - return null; + if (buffer.index !== finish || stack.length !== 1) { + throw new Error("Expected the serialized nodes to end at the constant pool with exactly one root"); } + + return stack[0]; } function scanConstant(constantIndex) { diff --git a/templates/lib/prism/serialize.rb.erb b/templates/lib/prism/serialize.rb.erb index d6e92ab5e7..6ab01834d4 100644 --- a/templates/lib/prism/serialize.rb.erb +++ b/templates/lib/prism/serialize.rb.erb @@ -50,7 +50,7 @@ module Prism constant_pool = ConstantPool.new(serialized, cpool_base, cpool_size) - node = loader.load_node(constant_pool, encoding, freeze) #: ProgramNode + node = loader.load_nodes(constant_pool, encoding, freeze, cpool_base) #: ProgramNode loader.load_constant_pool(constant_pool) raise unless loader.eof? @@ -181,7 +181,7 @@ module Prism constant_pool = ConstantPool.new(serialized, cpool_base, cpool_size) - node = loader.load_node(constant_pool, encoding, freeze) #: ProgramNode + node = loader.load_nodes(constant_pool, encoding, freeze, cpool_base) #: ProgramNode loader.load_constant_pool(constant_pool) raise unless loader.eof? @@ -531,14 +531,6 @@ module Prism (io.read(4) or raise).unpack1("L") #: Integer end - #: (ConstantPool constant_pool, Encoding encoding, bool freeze) -> node? - def load_optional_node(constant_pool, encoding, freeze) - if io.getbyte != 0 - io.pos -= 1 - load_node(constant_pool, encoding, freeze) - end - end - #: (Encoding encoding) -> String def load_string(encoding) (io.read(load_varuint) or raise).force_encoding(encoding).freeze @@ -588,69 +580,94 @@ module Prism end if RUBY_ENGINE == "ruby" - #: (ConstantPool constant_pool, Encoding encoding, bool freeze) -> node - def load_node(constant_pool, encoding, freeze) - type = io.getbyte - node_id = load_varuint - location = load_location(freeze) #: Location - value = - case type + # Load the nodes that are serialized before the given offset and return + # the root. Nodes are serialized in post-order, so the children of each + # node are the topmost values on the stack, in field order, by the time + # its record is read. + # + #: (ConstantPool constant_pool, Encoding encoding, bool freeze, Integer finish) -> node + def load_nodes(constant_pool, encoding, freeze, finish) + stack = [] #: Array[node?] + + while io.pos < finish + case (type = io.getbyte) + when 0 + stack << nil + when <%= Prism::Template::LAZY_MARKER %> + io.read(8) <%- nodes.each_with_index do |node, index| -%> when <%= index + 1 %> - <%- if node.needs_serialized_length? -%> - load_uint32 + node_id = load_varuint + location = load_location(freeze) + flags = load_varuint + <%- node.fields.each do |field| -%> + <%- case field -%> + <%- when Prism::Template::NodeField, Prism::Template::OptionalNodeField -%> + <%- when Prism::Template::NodeListField -%> + <%= field.name %>_size = load_varuint + <%- when Prism::Template::StringField -%> + field_<%= field.name %> = load_string(encoding) + <%- when Prism::Template::ConstantField -%> + field_<%= field.name %> = load_constant(constant_pool, encoding) + <%- when Prism::Template::OptionalConstantField -%> + field_<%= field.name %> = load_optional_constant(constant_pool, encoding) + <%- when Prism::Template::ConstantListField -%> + field_<%= field.name %> = Array.new(load_varuint) { load_constant(constant_pool, encoding) } + field_<%= field.name %>.freeze if freeze + <%- when Prism::Template::LocationField -%> + field_<%= field.name %> = load_location(freeze) + <%- when Prism::Template::OptionalLocationField -%> + field_<%= field.name %> = load_optional_location(freeze) + <%- when Prism::Template::UInt8Field -%> + field_<%= field.name %> = (io.getbyte or raise) + <%- when Prism::Template::UInt32Field -%> + field_<%= field.name %> = load_varuint + <%- when Prism::Template::IntegerField -%> + field_<%= field.name %> = load_integer + <%- when Prism::Template::DoubleField -%> + field_<%= field.name %> = load_double + <%- else raise -%> <%- end -%> - <%= node.name %>.new( - source, - node_id, - location, - load_varuint, - <%- node.fields.each do |field| -%> - <%- case field -%> - <%- when Prism::Template::NodeField -%> - load_node(constant_pool, encoding, freeze), #: <%= field.rbs_class %> - <%- when Prism::Template::OptionalNodeField -%> - load_optional_node(constant_pool, encoding, freeze), #: <%= field.rbs_class %> - <%- when Prism::Template::StringField -%> - load_string(encoding), - <%- when Prism::Template::NodeListField -%> - Array.new(load_varuint) do - load_node(constant_pool, encoding, freeze) #: <%= field.element_rbs_class %> - end.tap { |nodes| nodes.freeze if freeze }, - <%- when Prism::Template::ConstantField -%> - load_constant(constant_pool, encoding), - <%- when Prism::Template::OptionalConstantField -%> - load_optional_constant(constant_pool, encoding), - <%- when Prism::Template::ConstantListField -%> - Array.new(load_varuint) { load_constant(constant_pool, encoding) }.tap { |constants| constants.freeze if freeze }, - <%- when Prism::Template::LocationField -%> - load_location(freeze), - <%- when Prism::Template::OptionalLocationField -%> - load_optional_location(freeze), - <%- when Prism::Template::UInt8Field -%> - (io.getbyte or raise), - <%- when Prism::Template::UInt32Field -%> - load_varuint, - <%- when Prism::Template::IntegerField -%> - load_integer, - <%- when Prism::Template::DoubleField -%> - load_double, - <%- else raise -%> - <%- end -%> - <%- end -%> - ) + <%- end -%> + <%- node.child_fields.reverse_each do |field| -%> + <%- if field.is_a?(Prism::Template::NodeListField) -%> + field_<%= field.name %> = stack.pop(<%= field.name %>_size) #: <%= field.rbs_class %> + field_<%= field.name %>.freeze if freeze + <%- else -%> + field_<%= field.name %> = stack.pop #: <%= field.rbs_class %> + <%- end -%> + <%- end -%> + value = <%= node.name %>.new(source, node_id, location, flags<%= node.fields.map { |field| ", field_#{field.name}" }.join %>) + value.freeze if freeze + stack << value <%- end -%> else raise "Unknown node type: #{type}" end + end - value.freeze if freeze - value + raise unless io.pos == finish && stack.length == 1 + stack.first or raise end else # @rbs skip - def load_node(constant_pool, encoding, freeze) - @load_node_lambdas[(io.getbyte or raise)].call(constant_pool, encoding, freeze) + def load_nodes(constant_pool, encoding, freeze, finish) + stack = [] #: Array[node?] + + while io.pos < finish + type = (io.getbyte or raise) + + if type == 0 + stack << nil + elsif type == <%= Prism::Template::LAZY_MARKER %> + io.read(8) + else + stack << @load_node_lambdas[type].call(constant_pool, encoding, freeze, stack) + end + end + + raise unless io.pos == finish && stack.length == 1 + stack.first or raise end # @rbs skip @@ -658,52 +675,48 @@ module Prism @load_node_lambdas = [ nil, <%- nodes.each do |node| -%> - -> (constant_pool, encoding, freeze) { + -> (constant_pool, encoding, freeze, stack) { node_id = load_varuint location = load_location(freeze) - <%- if node.needs_serialized_length? -%> - load_uint32 + flags = load_varuint + <%- node.fields.each do |field| -%> + <%- case field -%> + <%- when Prism::Template::NodeField, Prism::Template::OptionalNodeField -%> + <%- when Prism::Template::NodeListField -%> + <%= field.name %>_size = load_varuint + <%- when Prism::Template::StringField -%> + field_<%= field.name %> = load_string(encoding) + <%- when Prism::Template::ConstantField -%> + field_<%= field.name %> = load_constant(constant_pool, encoding) + <%- when Prism::Template::OptionalConstantField -%> + field_<%= field.name %> = load_optional_constant(constant_pool, encoding) + <%- when Prism::Template::ConstantListField -%> + field_<%= field.name %> = Array.new(load_varuint) { load_constant(constant_pool, encoding) } + field_<%= field.name %>.freeze if freeze + <%- when Prism::Template::LocationField -%> + field_<%= field.name %> = load_location(freeze) + <%- when Prism::Template::OptionalLocationField -%> + field_<%= field.name %> = load_optional_location(freeze) + <%- when Prism::Template::UInt8Field -%> + field_<%= field.name %> = (io.getbyte or raise) + <%- when Prism::Template::UInt32Field -%> + field_<%= field.name %> = load_varuint + <%- when Prism::Template::IntegerField -%> + field_<%= field.name %> = load_integer + <%- when Prism::Template::DoubleField -%> + field_<%= field.name %> = load_double + <%- else raise -%> + <%- end -%> + <%- end -%> + <%- node.child_fields.reverse_each do |field| -%> + <%- if field.is_a?(Prism::Template::NodeListField) -%> + field_<%= field.name %> = stack.pop(<%= field.name %>_size) #: <%= field.rbs_class %> + field_<%= field.name %>.freeze if freeze + <%- else -%> + field_<%= field.name %> = stack.pop #: <%= field.rbs_class %> + <%- end -%> <%- end -%> - value = - <%= node.name %>.new( - source, - node_id, - location, - load_varuint, - <%- node.fields.map do |field| -%> - <%- case field -%> - <%- when Prism::Template::NodeField -%> - load_node(constant_pool, encoding, freeze), #: <%= field.rbs_class %> - <%- when Prism::Template::OptionalNodeField -%> - load_optional_node(constant_pool, encoding, freeze), #: <%= field.rbs_class %> - <%- when Prism::Template::StringField -%> - load_string(encoding), - <%- when Prism::Template::NodeListField -%> - Array.new(load_varuint) do - load_node(constant_pool, encoding, freeze) #: <%= field.element_rbs_class %> - end, - <%- when Prism::Template::ConstantField -%> - load_constant(constant_pool, encoding), - <%- when Prism::Template::OptionalConstantField -%> - load_optional_constant(constant_pool, encoding), - <%- when Prism::Template::ConstantListField -%> - Array.new(load_varuint) { load_constant(constant_pool, encoding) }, - <%- when Prism::Template::LocationField -%> - load_location(freeze), - <%- when Prism::Template::OptionalLocationField -%> - load_optional_location(freeze), - <%- when Prism::Template::UInt8Field -%> - (io.getbyte or raise), - <%- when Prism::Template::UInt32Field -%> - load_varuint, - <%- when Prism::Template::IntegerField -%> - load_integer, - <%- when Prism::Template::DoubleField -%> - load_double, - <%- else raise -%> - <%- end -%> - <%- end -%> - ) + value = <%= node.name %>.new(source, node_id, location, flags<%= node.fields.map { |field| ", field_#{field.name}" }.join %>) value.freeze if freeze value }, diff --git a/templates/src/serialize.c.erb b/templates/src/serialize.c.erb index dc27399c62..5d938b9c45 100644 --- a/templates/src/serialize.c.erb +++ b/templates/src/serialize.c.erb @@ -11,8 +11,10 @@ typedef int pm_serialize_unused_t; #else +#include "prism/compiler/branch.h" #include "prism/compiler/inline.h" +#include "prism/internal/allocator.h" #include "prism/internal/buffer.h" #include "prism/internal/comments.h" #include "prism/internal/diagnostic.h" @@ -27,7 +29,9 @@ typedef int pm_serialize_unused_t; #include "prism/line_offset_list.h" #include +#include #include +#include #include static PRISM_INLINE uint32_t @@ -69,91 +73,240 @@ pm_serialize_integer(const pm_integer_t *integer, pm_buffer_t *buffer) { } } +/** + * The kinds of entries on the serialization stack. + */ +typedef enum { + /** A node to serialize, or NULL for an absent optional node. */ + PM_SERIALIZE_ENTRY_NODE, + + /** Write the lazy marker and placeholders for its skip and length. */ + PM_SERIALIZE_ENTRY_LAZY_HEADER, + + /** Patch the skip, now that the lazy subtree is about to be written. */ + PM_SERIALIZE_ENTRY_LAZY_START, + + /** Patch the length, now that the lazy subtree has been written. */ + PM_SERIALIZE_ENTRY_LAZY_END +} pm_serialize_entry_kind_t; + +/** + * An entry on the serialization stack. + */ +typedef struct { + /** The node to serialize, for NODE entries. */ + const pm_node_t *node; + + /** + * For NODE entries, the offset of the placeholders that follow the node's + * lazy marker. For LAZY_* entries, the index of the NODE entry that owns + * them, which stays valid because that entry is beneath them. + */ + size_t offset; + + /** The kind of this entry. */ + pm_serialize_entry_kind_t kind; + + /** Whether the children of this node have already been pushed. */ + bool visited; +} pm_serialize_entry_t; + +/** + * The explicit stack used to walk the tree, so that serializing deeply nested + * trees does not depend on the size of the C stack. + */ +typedef struct { + /** The entries on the stack. */ + pm_serialize_entry_t *entries; + + /** The number of entries on the stack. */ + size_t size; + + /** The number of entries that fit in the allocated memory. */ + size_t capacity; +} pm_serialize_stack_t; + +/** + * Push an entry onto the serialization stack. This may reallocate the stack, + * which invalidates any pointers into it. + */ static void -pm_serialize_node(pm_parser_t *parser, pm_node_t *node, pm_buffer_t *buffer) { - pm_buffer_append_byte(buffer, (uint8_t) PM_NODE_TYPE(node)); - - <%- if Prism::Template::INCLUDE_NODE_ID -%> - pm_buffer_append_varuint(buffer, node->node_id); - <%- end -%> - pm_serialize_location(&node->location, buffer); - - switch (PM_NODE_TYPE(node)) { - // We do not need to serialize a ScopeNode ever as - // it is not part of the AST - case PM_SCOPE_NODE: - return; - <%- nodes.each do |node| -%> - case <%= node.type %>: { - <%- if node.needs_serialized_length? -%> - // serialize length - // encoding of location u32s make us need to save this offset. - size_t length_offset = buffer->length; - pm_buffer_append_string(buffer, "\0\0\0\0", 4); /* consume 4 bytes, updated below */ - <%- end -%> - <%- unless Prism::Template::SERIALIZE_ONLY_SEMANTICS_FIELDS && !node.flags -%> - pm_buffer_append_varuint(buffer, (uint32_t) node->flags); - <%- end -%> - <%- node.fields.each do |field| -%> - <%- case field -%> - <%- when Prism::Template::NodeField -%> - pm_serialize_node(parser, (pm_node_t *)((pm_<%= node.human %>_t *)node)-><%= field.name %>, buffer); - <%- when Prism::Template::OptionalNodeField -%> - if (((pm_<%= node.human %>_t *)node)-><%= field.name %> == NULL) { - pm_buffer_append_byte(buffer, 0); - } else { - pm_serialize_node(parser, (pm_node_t *)((pm_<%= node.human %>_t *)node)-><%= field.name %>, buffer); +pm_serialize_stack_push(pm_serialize_stack_t *stack, pm_serialize_entry_kind_t kind, const pm_node_t *node, size_t offset) { + if (PRISM_UNLIKELY(stack->size == stack->capacity)) { + size_t capacity = stack->capacity == 0 ? 64 : stack->capacity * 2; + pm_serialize_entry_t *entries = (pm_serialize_entry_t *) xrealloc_sized(stack->entries, capacity * sizeof(pm_serialize_entry_t), stack->capacity * sizeof(pm_serialize_entry_t)); + + if (PRISM_UNLIKELY(entries == NULL)) { + fprintf(stderr, "prism: out of memory; aborting\n"); + abort(); + } + + stack->entries = entries; + stack->capacity = capacity; + } + + stack->entries[stack->size++] = (pm_serialize_entry_t) { .node = node, .offset = offset, .kind = kind, .visited = false }; +} + +/** + * Serialize the tree rooted at the given node in post-order: the subtrees of a + * node's child fields are written in field order, followed by the node's own + * record. A loader can therefore build every node from a stack of already + * built children without recursing. + * + * The child subtrees of a node with a lazy field are preceded by the lazy + * marker, the byte length of the subtrees before the lazy field's subtree, and + * the byte length of the lazy field's subtree, so that a loader can skip it. + */ +static void +pm_serialize_node(pm_node_t *root, pm_buffer_t *buffer) { + pm_serialize_stack_t stack = { 0 }; + pm_serialize_stack_push(&stack, PM_SERIALIZE_ENTRY_NODE, root, 0); + + while (stack.size > 0) { + pm_serialize_entry_t *entry = &stack.entries[--stack.size]; + + switch (entry->kind) { + case PM_SERIALIZE_ENTRY_LAZY_HEADER: { + pm_buffer_append_byte(buffer, <%= Prism::Template::LAZY_MARKER %>); + stack.entries[entry->offset].offset = buffer->length; + pm_buffer_append_zeroes(buffer, 8); + continue; } - <%- when Prism::Template::StringField -%> - pm_serialize_string(&((pm_<%= node.human %>_t *)node)-><%= field.name %>, buffer); - <%- when Prism::Template::NodeListField -%> - uint32_t <%= field.name %>_size = pm_sizet_to_u32(((pm_<%= node.human %>_t *)node)-><%= field.name %>.size); - pm_buffer_append_varuint(buffer, <%= field.name %>_size); - for (uint32_t index = 0; index < <%= field.name %>_size; index++) { - pm_serialize_node(parser, (pm_node_t *) ((pm_<%= node.human %>_t *)node)-><%= field.name %>.nodes[index], buffer); + case PM_SERIALIZE_ENTRY_LAZY_START: { + size_t offset = stack.entries[entry->offset].offset; + uint32_t skip = pm_sizet_to_u32(buffer->length - offset - 8); + memcpy(buffer->value + offset, &skip, sizeof(uint32_t)); + continue; } - <%- when Prism::Template::ConstantField, Prism::Template::OptionalConstantField -%> - pm_buffer_append_varuint(buffer, pm_sizet_to_u32(((pm_<%= node.human %>_t *)node)-><%= field.name %>)); - <%- when Prism::Template::ConstantListField -%> - uint32_t <%= field.name %>_size = pm_sizet_to_u32(((pm_<%= node.human %>_t *)node)-><%= field.name %>.size); - pm_buffer_append_varuint(buffer, <%= field.name %>_size); - for (uint32_t index = 0; index < <%= field.name %>_size; index++) { - pm_buffer_append_varuint(buffer, pm_sizet_to_u32(((pm_<%= node.human %>_t *)node)-><%= field.name %>.ids[index])); + case PM_SERIALIZE_ENTRY_LAZY_END: { + size_t offset = stack.entries[entry->offset].offset; + uint32_t skip; + memcpy(&skip, buffer->value + offset, sizeof(uint32_t)); + + uint32_t length = pm_sizet_to_u32(buffer->length - offset - 8 - skip); + memcpy(buffer->value + offset + 4, &length, sizeof(uint32_t)); + continue; } - <%- when Prism::Template::LocationField -%> - <%- if field.should_be_serialized? -%> - pm_serialize_location(&((pm_<%= node.human %>_t *)node)-><%= field.name %>, buffer); - <%- end -%> - <%- when Prism::Template::OptionalLocationField -%> - <%- if field.should_be_serialized? -%> - if (((pm_<%= node.human %>_t *)node)-><%= field.name %>.length == 0) { - pm_buffer_append_byte(buffer, 0); - } else { - pm_buffer_append_byte(buffer, 1); - pm_serialize_location(&((pm_<%= node.human %>_t *)node)-><%= field.name %>, buffer); + case PM_SERIALIZE_ENTRY_NODE: + break; + } + + const pm_node_t *node = entry->node; + + if (node == NULL) { + pm_buffer_append_byte(buffer, 0); + continue; + } + + if (!entry->visited) { + /* Leave the node on the stack beneath its children, which are + * pushed in reverse field order so that they are popped, and + * therefore written, in field order. A node without children is + * written straight away instead. */ + entry->visited = true; + size_t index = stack.size++; + + switch (PM_NODE_TYPE(node)) { + <%- nodes.each do |node| -%> + <%- next if node.child_fields.empty? -%> + case <%= node.type %>: { + const pm_<%= node.human %>_t *cast = (const pm_<%= node.human %>_t *) node; + <%- node.child_fields.reverse_each do |field| -%> + <%- case field -%> + <%- when Prism::Template::NodeListField -%> + for (size_t list_index = cast-><%= field.name %>.size; list_index > 0; list_index--) { + pm_serialize_stack_push(&stack, PM_SERIALIZE_ENTRY_NODE, cast-><%= field.name %>.nodes[list_index - 1], 0); + } + <%- else -%> + <%- if field == node.lazy_field -%> + pm_serialize_stack_push(&stack, PM_SERIALIZE_ENTRY_LAZY_END, NULL, index); + pm_serialize_stack_push(&stack, PM_SERIALIZE_ENTRY_NODE, (const pm_node_t *) cast-><%= field.name %>, 0); + pm_serialize_stack_push(&stack, PM_SERIALIZE_ENTRY_LAZY_START, NULL, index); + <%- else -%> + pm_serialize_stack_push(&stack, PM_SERIALIZE_ENTRY_NODE, (const pm_node_t *) cast-><%= field.name %>, 0); + <%- end -%> + <%- end -%> + <%- end -%> + <%- if node.lazy_field -%> + pm_serialize_stack_push(&stack, PM_SERIALIZE_ENTRY_LAZY_HEADER, NULL, index); + <%- end -%> + break; + } + <%- end -%> + default: + break; } - <%- end -%> - <%- when Prism::Template::UInt8Field -%> - pm_buffer_append_byte(buffer, ((pm_<%= node.human %>_t *)node)-><%= field.name %>); - <%- when Prism::Template::UInt32Field -%> - pm_buffer_append_varuint(buffer, ((pm_<%= node.human %>_t *)node)-><%= field.name %>); - <%- when Prism::Template::IntegerField -%> - pm_serialize_integer(&((pm_<%= node.human %>_t *)node)-><%= field.name %>, buffer); - <%- when Prism::Template::DoubleField -%> - pm_buffer_append_double(buffer, ((pm_<%= node.human %>_t *)node)-><%= field.name %>); - <%- else -%> - <%- raise -%> - <%- end -%> - <%- end -%> - <%- if node.needs_serialized_length? -%> - // serialize length - uint32_t length = pm_sizet_to_u32(buffer->length - length_offset); - memcpy(buffer->value + length_offset, &length, sizeof(uint32_t)); - <%- end -%> - break; + + if (stack.size != index + 1) continue; + stack.size--; } + + pm_buffer_append_byte(buffer, (uint8_t) PM_NODE_TYPE(node)); + <%- if Prism::Template::INCLUDE_NODE_ID -%> + pm_buffer_append_varuint(buffer, node->node_id); <%- end -%> + pm_serialize_location(&node->location, buffer); + + switch (PM_NODE_TYPE(node)) { + /* A ScopeNode is never part of the tree, so it has no fields to + * serialize. */ + case PM_SCOPE_NODE: + break; + <%- nodes.each do |node| -%> + case <%= node.type %>: { + <%- if node.fields.any? { |field| !field.is_a?(Prism::Template::NodeField) && !field.is_a?(Prism::Template::OptionalNodeField) && field.should_be_serialized? } -%> + const pm_<%= node.human %>_t *cast = (const pm_<%= node.human %>_t *) node; + <%- end -%> + <%- unless Prism::Template::SERIALIZE_ONLY_SEMANTICS_FIELDS && !node.flags -%> + pm_buffer_append_varuint(buffer, (uint32_t) node->flags); + <%- end -%> + <%- node.fields.each do |field| -%> + <%- case field -%> + <%- when Prism::Template::NodeField, Prism::Template::OptionalNodeField -%> + <%- when Prism::Template::NodeListField -%> + pm_buffer_append_varuint(buffer, pm_sizet_to_u32(cast-><%= field.name %>.size)); + <%- when Prism::Template::StringField -%> + pm_serialize_string(&cast-><%= field.name %>, buffer); + <%- when Prism::Template::ConstantField, Prism::Template::OptionalConstantField -%> + pm_buffer_append_varuint(buffer, pm_sizet_to_u32(cast-><%= field.name %>)); + <%- when Prism::Template::ConstantListField -%> + pm_buffer_append_varuint(buffer, pm_sizet_to_u32(cast-><%= field.name %>.size)); + for (size_t index = 0; index < cast-><%= field.name %>.size; index++) { + pm_buffer_append_varuint(buffer, pm_sizet_to_u32(cast-><%= field.name %>.ids[index])); + } + <%- when Prism::Template::LocationField -%> + <%- if field.should_be_serialized? -%> + pm_serialize_location(&cast-><%= field.name %>, buffer); + <%- end -%> + <%- when Prism::Template::OptionalLocationField -%> + <%- if field.should_be_serialized? -%> + if (cast-><%= field.name %>.length == 0) { + pm_buffer_append_byte(buffer, 0); + } else { + pm_buffer_append_byte(buffer, 1); + pm_serialize_location(&cast-><%= field.name %>, buffer); + } + <%- end -%> + <%- when Prism::Template::UInt8Field -%> + pm_buffer_append_byte(buffer, cast-><%= field.name %>); + <%- when Prism::Template::UInt32Field -%> + pm_buffer_append_varuint(buffer, cast-><%= field.name %>); + <%- when Prism::Template::IntegerField -%> + pm_serialize_integer(&cast-><%= field.name %>, buffer); + <%- when Prism::Template::DoubleField -%> + pm_buffer_append_double(buffer, cast-><%= field.name %>); + <%- else -%> + <%- raise -%> + <%- end -%> + <%- end -%> + break; + } + <%- end -%> + } } + + xfree_sized(stack.entries, stack.capacity * sizeof(pm_serialize_entry_t)); } /* @@ -322,7 +475,7 @@ pm_serialize_content(pm_parser_t *parser, pm_node_t *node, pm_buffer_t *buffer) pm_buffer_append_varuint(buffer, parser->constant_pool.size); // Now we're going to serialize the content of the node. - pm_serialize_node(parser, node, buffer); + pm_serialize_node(node, buffer); // Now we're going to serialize the offset of the constant pool back where // we left space for it. diff --git a/templates/template.rb b/templates/template.rb index 5870d649d4..9c4a6ecd66 100755 --- a/templates/template.rb +++ b/templates/template.rb @@ -16,6 +16,12 @@ module Template # :nodoc: all COMMON_FLAGS_COUNT = 2 + # The byte that precedes the child subtrees of a node with a lazy field in + # the serialized output (see NodeType#lazy_field). Node types are serialized + # as 1 through the number of node types and 0 marks an absent optional node, + # so this must be greater than the number of node types. + LAZY_MARKER = 0xFF + class Error attr_reader :name @@ -555,10 +561,19 @@ def semantic_fields @semantic_fields ||= @fields.select(&:semantic_field?) end - # Should emit serialized length of node so implementations can skip - # the node to enable lazy parsing. - def needs_serialized_length? - name == "DefNode" + # The fields that hold child nodes. In the serialized output, their + # subtrees are written in field order ahead of this node's own record. + def child_fields + @child_fields ||= fields.grep(NodeKindField) + end + + # The field whose subtree loaders can skip and deserialize on demand. In + # the serialized output, the node's child subtrees are preceded by + # LAZY_MARKER and two uint32 values: the byte length of the subtrees + # before this field's subtree, and the byte length of this field's + # subtree. + def lazy_field + fields.find { |field| field.name == "body" } if name == "DefNode" end private @@ -700,10 +715,19 @@ def locals config = YAML.load_file(File.expand_path("../config.yml", __dir__)) flags = config.fetch("flags").to_h { |flags| [flags["name"], Flags.new(flags)] } + nodes = config.fetch("nodes").map { |node| NodeType.new(node, flags) }.sort_by(&:name) + raise "Too many node types to reserve LAZY_MARKER" if nodes.length >= LAZY_MARKER + + nodes.each do |node| + if (field = node.lazy_field) && !field.is_a?(OptionalNodeField) + raise "Lazy field #{node.name}##{field.name} must be an optional node field" + end + end + { errors: config.fetch("errors").map { |name| Error.new(name) }, warnings: config.fetch("warnings").map { |name| Warning.new(name) }, - nodes: config.fetch("nodes").map { |node| NodeType.new(node, flags) }.sort_by(&:name), + nodes: nodes, tokens: config.fetch("tokens").map { |token| Token.new(token) }, flags: flags.values } diff --git a/test/prism/api/dump_test.rb b/test/prism/api/dump_test.rb index 941088e159..28df7c3005 100644 --- a/test/prism/api/dump_test.rb +++ b/test/prism/api/dump_test.rb @@ -42,6 +42,16 @@ def test_dump_file end end + # These trees are each 20,000 nodes deep, which is deeper than the stack of + # a thread allows a walk of the tree to recurse. + def test_dump_and_load_deep_trees + ["1" + "+1" * 20_000, "a" + ".b" * 20_000, "A" + "::B" * 20_000].each do |source| + Thread.new do + assert_kind_of ProgramNode, Prism.load(source, Prism.dump(source)).value + end.join + end + end + private def assert_dump(fixture)