Skip to content

Commit 8e6a93b

Browse files
committed
Freeze AST option
To make it so that you can pass `freeze: true` to Prism parse methods and get back a deeply-frozen AST that is Ractor- shareable.
1 parent 8d9d429 commit 8e6a93b

File tree

14 files changed

+586
-229
lines changed

14 files changed

+586
-229
lines changed

docs/ruby_api.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ The full API is documented below.
2323
* `Prism.parse_stream(io)` - parse the syntax tree corresponding to the source that is read out of the given IO object using the `#gets` method and return it within a parse result
2424
* `Prism.parse_lex(source)` - parse the syntax tree corresponding to the given source string and return it within a parse result, along with the tokens
2525
* `Prism.parse_lex_file(filepath)` - parse the syntax tree corresponding to the given source file and return it within a parse result, along with the tokens
26-
* `Prism.load(source, serialized)` - load the serialized syntax tree using the source as a reference into a syntax tree
26+
* `Prism.load(source, serialized, freeze = false)` - load the serialized syntax tree using the source as a reference into a syntax tree
2727
* `Prism.parse_comments(source)` - parse the comments corresponding to the given source string and return them
2828
* `Prism.parse_file_comments(source)` - parse the comments corresponding to the given source file and return them
2929
* `Prism.parse_success?(source)` - parse the syntax tree corresponding to the given source string and return true if it was parsed without errors

ext/prism/extension.c

Lines changed: 137 additions & 58 deletions
Large diffs are not rendered by default.

ext/prism/extension.h

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@
77
#include <ruby/encoding.h>
88
#include "prism.h"
99

10-
VALUE pm_source_new(const pm_parser_t *parser, rb_encoding *encoding);
11-
VALUE pm_token_new(const pm_parser_t *parser, const pm_token_t *token, rb_encoding *encoding, VALUE source);
12-
VALUE pm_ast_new(const pm_parser_t *parser, const pm_node_t *node, rb_encoding *encoding, VALUE source);
10+
VALUE pm_source_new(const pm_parser_t *parser, rb_encoding *encoding, bool freeze);
11+
VALUE pm_token_new(const pm_parser_t *parser, const pm_token_t *token, rb_encoding *encoding, VALUE source, bool freeze);
12+
VALUE pm_ast_new(const pm_parser_t *parser, const pm_node_t *node, rb_encoding *encoding, VALUE source, bool freeze);
1313
VALUE pm_integer_new(const pm_integer_t *integer);
1414

1515
void Init_prism_api_node(void);

include/prism/options.h

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,13 @@ typedef struct pm_options {
160160
* inside another script.
161161
*/
162162
bool partial_script;
163+
164+
/**
165+
* Whether or not the parser should freeze the nodes that it creates. This
166+
* makes it possible to have a deeply frozen AST that is safe to share
167+
* between concurrency primitives.
168+
*/
169+
bool freeze;
163170
} pm_options_t;
164171

165172
/**
@@ -285,6 +292,14 @@ PRISM_EXPORTED_FUNCTION void pm_options_main_script_set(pm_options_t *options, b
285292
*/
286293
PRISM_EXPORTED_FUNCTION void pm_options_partial_script_set(pm_options_t *options, bool partial_script);
287294

295+
/**
296+
* Set the freeze option on the given options struct.
297+
*
298+
* @param options The options struct to set the freeze value on.
299+
* @param freeze The freeze value to set.
300+
*/
301+
PRISM_EXPORTED_FUNCTION void pm_options_freeze_set(pm_options_t *options, bool freeze);
302+
288303
/**
289304
* Allocate and zero out the scopes array on the given options struct.
290305
*
@@ -355,6 +370,7 @@ PRISM_EXPORTED_FUNCTION void pm_options_free(pm_options_t *options);
355370
* | `1` | encoding locked |
356371
* | `1` | main script |
357372
* | `1` | partial script |
373+
* | `1` | freeze |
358374
* | `4` | the number of scopes |
359375
* | ... | the scopes |
360376
*

java/org/prism/ParsingOptions.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,9 @@ public static byte[] serialize(byte[] filepath, int line, byte[] encoding, boole
8282
// partialScript
8383
output.write(partialScript ? 1 : 0);
8484

85+
// freeze
86+
output.write(0);
87+
8588
// scopes
8689

8790
// number of scopes

javascript/src/parsePrism.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,9 @@ function dumpOptions(options) {
122122
template.push("C");
123123
values.push(dumpBooleanOption(options.partial_script));
124124

125+
template.push("C");
126+
values.push(0);
127+
125128
template.push("L");
126129
if (options.scopes) {
127130
const scopes = options.scopes;

lib/prism.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,11 +59,11 @@ def self.lex_ripper(source)
5959
end
6060

6161
# :call-seq:
62-
# Prism::load(source, serialized) -> ParseResult
62+
# Prism::load(source, serialized, freeze) -> ParseResult
6363
#
6464
# Load the serialized AST using the source as a reference into a tree.
65-
def self.load(source, serialized)
66-
Serialize.load(source, serialized)
65+
def self.load(source, serialized, freeze = false)
66+
Serialize.load(source, serialized, freeze)
6767
end
6868
end
6969

lib/prism/ffi.rb

Lines changed: 60 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -279,7 +279,7 @@ def parse_stream(stream, **options)
279279
# access to the IO object already through the closure of the lambda, we
280280
# can pass a null pointer here and not worry.
281281
LibRubyParser.pm_serialize_parse_stream(buffer.pointer, nil, callback, dump_options(options))
282-
Prism.load(source, buffer.read)
282+
Prism.load(source, buffer.read, options.fetch(:freeze, false))
283283
end
284284
end
285285

@@ -354,22 +354,37 @@ def profile_file(filepath, **options)
354354
def dump_common(string, options) # :nodoc:
355355
LibRubyParser::PrismBuffer.with do |buffer|
356356
LibRubyParser.pm_serialize_parse(buffer.pointer, string.pointer, string.length, dump_options(options))
357-
buffer.read
357+
358+
dumped = buffer.read
359+
dumped.freeze if options.fetch(:freeze, false)
360+
361+
dumped
358362
end
359363
end
360364

361365
def lex_common(string, code, options) # :nodoc:
362-
serialized = LibRubyParser::PrismBuffer.with do |buffer|
363-
LibRubyParser.pm_serialize_lex(buffer.pointer, string.pointer, string.length, dump_options(options))
364-
buffer.read
366+
serialized =
367+
LibRubyParser::PrismBuffer.with do |buffer|
368+
LibRubyParser.pm_serialize_lex(buffer.pointer, string.pointer, string.length, dump_options(options))
369+
buffer.read
370+
end
371+
372+
freeze = options.fetch(:freeze, false)
373+
source = Source.for(code)
374+
result = Serialize.load_tokens(source, serialized, freeze)
375+
376+
if freeze
377+
source.source.freeze
378+
source.offsets.freeze
379+
source.freeze
365380
end
366381

367-
Serialize.load_tokens(Source.for(code), serialized)
382+
result
368383
end
369384

370385
def parse_common(string, code, options) # :nodoc:
371386
serialized = dump_common(string, options)
372-
Prism.load(code, serialized)
387+
Prism.load(code, serialized, options.fetch(:freeze, false))
373388
end
374389

375390
def parse_comments_common(string, code, options) # :nodoc:
@@ -382,7 +397,14 @@ def parse_comments_common(string, code, options) # :nodoc:
382397
loader.load_header
383398
loader.load_encoding
384399
loader.load_start_line
385-
loader.load_comments
400+
401+
if (freeze = options.fetch(:freeze, false))
402+
source.source.freeze
403+
source.offsets.freeze
404+
source.freeze
405+
end
406+
407+
loader.load_comments(freeze)
386408
end
387409
end
388410

@@ -392,12 +414,35 @@ def parse_lex_common(string, code, options) # :nodoc:
392414

393415
source = Source.for(code)
394416
loader = Serialize::Loader.new(source, buffer.read)
417+
freeze = options.fetch(:freeze, false)
395418

396-
tokens = loader.load_tokens
397-
node, comments, magic_comments, data_loc, errors, warnings = loader.load_nodes
398-
tokens.each { |token,| token.value.force_encoding(loader.encoding) }
419+
tokens = loader.load_tokens(false)
420+
node, comments, magic_comments, data_loc, errors, warnings = loader.load_nodes(freeze)
399421

400-
ParseLexResult.new([node, tokens], comments, magic_comments, data_loc, errors, warnings, source)
422+
tokens.each do |token,|
423+
token.value.force_encoding(loader.encoding)
424+
425+
if freeze
426+
token.value.freeze
427+
token.location.freeze
428+
token.freeze
429+
end
430+
end
431+
432+
value = [node, tokens]
433+
result = ParseLexResult.new(value, comments, magic_comments, data_loc, errors, warnings, source)
434+
435+
if freeze
436+
source.source.freeze
437+
source.offsets.freeze
438+
source.freeze
439+
tokens.each(&:freeze)
440+
tokens.freeze
441+
value.freeze
442+
result.freeze
443+
end
444+
445+
result
401446
end
402447
end
403448

@@ -482,6 +527,9 @@ def dump_options(options)
482527
template << "C"
483528
values << (options.fetch(:partial_script, false) ? 1 : 0)
484529

530+
template << "C"
531+
values << (options.fetch(:freeze, false) ? 1 : 0)
532+
485533
template << "L"
486534
if (scopes = options[:scopes])
487535
values << scopes.length

0 commit comments

Comments
 (0)