2023-09-13 00:11:45 -07:00
|
|
|
const std = @import("std");
|
|
|
|
|
2023-09-24 18:22:12 -07:00
|
|
|
const buffers = @import("./linebuffer.zig");
|
|
|
|
const tokenizer = @import("./tokenizer.zig");
|
|
|
|
const Value = @import("./parser/value.zig").Value;
|
2023-09-21 23:34:17 -07:00
|
|
|
|
2023-09-13 00:11:45 -07:00
|
|
|
pub const Diagnostics = struct {
|
|
|
|
row: usize,
|
|
|
|
span: struct { absolute: usize, line_offset: usize, length: usize },
|
|
|
|
message: []const u8,
|
|
|
|
};
|
|
|
|
|
2023-09-24 18:22:12 -07:00
|
|
|
pub const Error = error{
|
|
|
|
UnexpectedIndent,
|
|
|
|
UnexpectedValue,
|
|
|
|
ExtraContent,
|
|
|
|
EmptyDocument,
|
|
|
|
DuplicateKey,
|
|
|
|
BadMapEntry,
|
|
|
|
BadState,
|
|
|
|
BadToken,
|
|
|
|
Fail,
|
|
|
|
} || tokenizer.Error || std.mem.Allocator.Error;
|
|
|
|
|
|
|
|
pub const DuplicateKeyBehavior = enum {
|
|
|
|
use_first,
|
|
|
|
use_last,
|
|
|
|
fail,
|
2023-09-21 23:34:17 -07:00
|
|
|
};
|
|
|
|
|
2023-09-24 18:22:12 -07:00
|
|
|
pub const DefaultObject = enum {
|
|
|
|
scalar,
|
|
|
|
string,
|
|
|
|
list,
|
|
|
|
map,
|
|
|
|
fail,
|
2023-09-21 23:34:17 -07:00
|
|
|
};
|
|
|
|
|
2023-09-24 18:22:12 -07:00
|
|
|
const ParseState = enum { initial, value, done };
|
2023-09-21 23:34:17 -07:00
|
|
|
|
2023-09-24 18:22:12 -07:00
|
|
|
pub const Document = struct {
|
|
|
|
arena: std.heap.ArenaAllocator,
|
|
|
|
root: Value,
|
2023-09-21 23:34:17 -07:00
|
|
|
|
2023-09-24 18:22:12 -07:00
|
|
|
pub fn init(alloc: std.mem.Allocator) Document {
|
|
|
|
return .{
|
|
|
|
.arena = std.heap.ArenaAllocator.init(alloc),
|
|
|
|
.root = undefined,
|
2023-09-21 23:34:17 -07:00
|
|
|
};
|
|
|
|
}
|
2023-09-17 23:09:26 -07:00
|
|
|
|
2023-09-24 18:22:12 -07:00
|
|
|
pub fn printDebug(self: Document) void {
|
|
|
|
return self.root.printDebug();
|
config: differentiate fields in Value
This makes handling Value very slightly more work, but it provides
useful metadata that can be used to perform better conversion and
serialization.
The motivation behind the "scalar" type is that in general, only
scalars can be coerced to other types. For example, a scalar `null`
and a string `> null` have the same in-memory representation. If they
are treated identically, this precludes unambiguously converting an
optional string whose contents are "null". With the two disambiguated,
we can choose to convert `null` to the null object and `> null` to a
string of contents "null". This ambiguity does not necessary exist for
the standard boolean values `true` and `false`, but it does allow the
conversion to be more strict, and it will theoretically result in
documents that read more naturally.
The motivation behind exposing flow_list and flow_map is that it will
allow preserving document formatting round trip (well, this isn't
strictly true: single line explicit strings neither remember whether
they were line strings or space strings, and they don't remember if
they were indented. However, that is much less information to lose).
The following formulations will parse to the same indistinguishable
value:
key: > value
key:
> value
key: | value
key:
| value
I think that's okay. It's a lot easier to chose a canonical form for
this case than it is for a map/list without any hints regarding its
origin.
2023-09-19 00:14:29 -07:00
|
|
|
}
|
|
|
|
|
2023-09-24 18:22:12 -07:00
|
|
|
pub fn deinit(self: Document) void {
|
|
|
|
self.arena.deinit();
|
config: start doing some code cleanup
I was pretty sloppy with the code organization while writing out the
state machines because my focus was on thinking through the parsing
process and logic there. However, The code was not in good shape to
continue implementing code features (not document features). This is
the first of probably several commits that will work on cleaning up
some things.
Value has been promoted to the top level namespace, and Document has an
initializer function. Referencing Value.List and Value.Map are much
cleaner now. Type aliases are good.
For the flow parser, `popStack` does not have to access anything except
the current stack. This can be passed in as a parameter. This means
that `parse` is ready to be refactored to take a buffer and an
allocator.
The main next steps for code improvement are:
1. reentrant/streaming parser. I am planning to leave it as
line-buffered, though I could go further. Line-buffered has two main
benefits: the tokenizer doesn't need to be refactored significantly,
and the flow parser doesn't need to be made reentrant. I may
reevaluate this as I am implementing it, however, as those changes
may be simpler than I think.
2. Actually implement the error diagnostics info. I have some skeleton
structure in place for this, so it should just be doing the work of
getting it hooked up.
3. Parse into object. Metaprogramming, let's go. It will be interesting
to try to do this non-recursively, as well (curious to see if it
results in code bloat).
4. Object to Document. This is probably going to be annoying, since
there are a variety of edge cases that will have to be handled. And
lots of objects that cannot be represented as documents.
5. Serialize Document. One thing the parser does not preserve is
whether a Value was flow-style or not, so it will be impossible to
do round-trip formatting preservation. That's currently a non-goal,
and I haven't decided yet if flow-style output should be based on
some heuristic (number/length of values in container) or just never
emitted. Lack of round-trip preservation does make using this as a
general purpose config format a lot more dubious, so I will have to
think about this some more.
6. Document to JSON. Why not? I will hand roll this and it will suck.
And then everything will be perfect and never need to be touched again.
2023-09-18 00:01:36 -07:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2023-09-13 00:11:45 -07:00
|
|
|
pub const Parser = struct {
|
|
|
|
allocator: std.mem.Allocator,
|
|
|
|
dupe_behavior: DuplicateKeyBehavior = .fail,
|
|
|
|
default_object: DefaultObject = .fail,
|
|
|
|
diagnostics: Diagnostics = .{
|
|
|
|
.row = 0,
|
|
|
|
.span = .{ .absolute = 0, .line_offset = 0, .length = 0 },
|
|
|
|
.message = "all is well",
|
|
|
|
},
|
|
|
|
|
config: differentiate fields in Value
This makes handling Value very slightly more work, but it provides
useful metadata that can be used to perform better conversion and
serialization.
The motivation behind the "scalar" type is that in general, only
scalars can be coerced to other types. For example, a scalar `null`
and a string `> null` have the same in-memory representation. If they
are treated identically, this precludes unambiguously converting an
optional string whose contents are "null". With the two disambiguated,
we can choose to convert `null` to the null object and `> null` to a
string of contents "null". This ambiguity does not necessary exist for
the standard boolean values `true` and `false`, but it does allow the
conversion to be more strict, and it will theoretically result in
documents that read more naturally.
The motivation behind exposing flow_list and flow_map is that it will
allow preserving document formatting round trip (well, this isn't
strictly true: single line explicit strings neither remember whether
they were line strings or space strings, and they don't remember if
they were indented. However, that is much less information to lose).
The following formulations will parse to the same indistinguishable
value:
key: > value
key:
> value
key: | value
key:
| value
I think that's okay. It's a lot easier to chose a canonical form for
this case than it is for a map/list without any hints regarding its
origin.
2023-09-19 00:14:29 -07:00
|
|
|
pub const State = struct {
|
|
|
|
pub const Stack = std.ArrayList(*Value);
|
|
|
|
|
|
|
|
document: Document,
|
|
|
|
value_stack: Stack,
|
2023-09-24 18:22:12 -07:00
|
|
|
state: enum { initial, value, done } = .initial,
|
|
|
|
expect_shift: tokenizer.ShiftDirection = .none,
|
config: differentiate fields in Value
This makes handling Value very slightly more work, but it provides
useful metadata that can be used to perform better conversion and
serialization.
The motivation behind the "scalar" type is that in general, only
scalars can be coerced to other types. For example, a scalar `null`
and a string `> null` have the same in-memory representation. If they
are treated identically, this precludes unambiguously converting an
optional string whose contents are "null". With the two disambiguated,
we can choose to convert `null` to the null object and `> null` to a
string of contents "null". This ambiguity does not necessary exist for
the standard boolean values `true` and `false`, but it does allow the
conversion to be more strict, and it will theoretically result in
documents that read more naturally.
The motivation behind exposing flow_list and flow_map is that it will
allow preserving document formatting round trip (well, this isn't
strictly true: single line explicit strings neither remember whether
they were line strings or space strings, and they don't remember if
they were indented. However, that is much less information to lose).
The following formulations will parse to the same indistinguishable
value:
key: > value
key:
> value
key: | value
key:
| value
I think that's okay. It's a lot easier to chose a canonical form for
this case than it is for a map/list without any hints regarding its
origin.
2023-09-19 00:14:29 -07:00
|
|
|
dangling_key: ?[]const u8 = null,
|
|
|
|
|
|
|
|
pub fn init(alloc: std.mem.Allocator) State {
|
|
|
|
return .{
|
|
|
|
.document = Document.init(alloc),
|
|
|
|
.value_stack = Stack.init(alloc),
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn deinit(self: State) void {
|
|
|
|
self.value_stack.deinit();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2023-09-14 23:38:24 -07:00
|
|
|
pub fn parseBuffer(self: *Parser, buffer: []const u8) Error!Document {
|
config: start doing some code cleanup
I was pretty sloppy with the code organization while writing out the
state machines because my focus was on thinking through the parsing
process and logic there. However, The code was not in good shape to
continue implementing code features (not document features). This is
the first of probably several commits that will work on cleaning up
some things.
Value has been promoted to the top level namespace, and Document has an
initializer function. Referencing Value.List and Value.Map are much
cleaner now. Type aliases are good.
For the flow parser, `popStack` does not have to access anything except
the current stack. This can be passed in as a parameter. This means
that `parse` is ready to be refactored to take a buffer and an
allocator.
The main next steps for code improvement are:
1. reentrant/streaming parser. I am planning to leave it as
line-buffered, though I could go further. Line-buffered has two main
benefits: the tokenizer doesn't need to be refactored significantly,
and the flow parser doesn't need to be made reentrant. I may
reevaluate this as I am implementing it, however, as those changes
may be simpler than I think.
2. Actually implement the error diagnostics info. I have some skeleton
structure in place for this, so it should just be doing the work of
getting it hooked up.
3. Parse into object. Metaprogramming, let's go. It will be interesting
to try to do this non-recursively, as well (curious to see if it
results in code bloat).
4. Object to Document. This is probably going to be annoying, since
there are a variety of edge cases that will have to be handled. And
lots of objects that cannot be represented as documents.
5. Serialize Document. One thing the parser does not preserve is
whether a Value was flow-style or not, so it will be impossible to
do round-trip formatting preservation. That's currently a non-goal,
and I haven't decided yet if flow-style output should be based on
some heuristic (number/length of values in container) or just never
emitted. Lack of round-trip preservation does make using this as a
general purpose config format a lot more dubious, so I will have to
think about this some more.
6. Document to JSON. Why not? I will hand roll this and it will suck.
And then everything will be perfect and never need to be touched again.
2023-09-18 00:01:36 -07:00
|
|
|
var document = Document.init(self.allocator);
|
2023-09-13 00:11:45 -07:00
|
|
|
errdefer document.deinit();
|
|
|
|
const arena_alloc = document.arena.allocator();
|
|
|
|
|
|
|
|
var state: ParseState = .initial;
|
2023-09-24 18:22:12 -07:00
|
|
|
var expect_shift: tokenizer.ShiftDirection = .none;
|
2023-09-17 19:28:07 -07:00
|
|
|
var dangling_key: ?[]const u8 = null;
|
2023-09-13 00:11:45 -07:00
|
|
|
var stack = std.ArrayList(*Value).init(arena_alloc);
|
|
|
|
defer stack.deinit();
|
|
|
|
|
2023-09-24 18:22:12 -07:00
|
|
|
var tok: tokenizer.LineTokenizer(buffers.FixedLineBuffer) = .{
|
|
|
|
.buffer = buffers.FixedLineBuffer.init(buffer),
|
2023-09-21 23:34:17 -07:00
|
|
|
.diagnostics = &self.diagnostics,
|
|
|
|
};
|
|
|
|
|
2023-09-13 00:11:45 -07:00
|
|
|
while (try tok.next()) |line| {
|
|
|
|
if (line.contents == .comment) continue;
|
|
|
|
|
|
|
|
var flip = true;
|
|
|
|
var flop = false;
|
|
|
|
// this is needed to give us a second go round when the line is dedented
|
|
|
|
flipflop: while (flip) : (flop = true) {
|
|
|
|
switch (state) {
|
|
|
|
.initial => {
|
2023-09-24 18:22:12 -07:00
|
|
|
if (line.shift == .indent) return error.UnexpectedIndent;
|
2023-09-13 00:11:45 -07:00
|
|
|
|
|
|
|
switch (line.contents) {
|
|
|
|
// we filter out comments above
|
|
|
|
.comment => unreachable,
|
|
|
|
.in_line => |in_line| switch (in_line) {
|
|
|
|
// empty scalars are only emitted for a list_item or a map_item
|
|
|
|
.empty => unreachable,
|
|
|
|
.scalar => |str| {
|
config: differentiate fields in Value
This makes handling Value very slightly more work, but it provides
useful metadata that can be used to perform better conversion and
serialization.
The motivation behind the "scalar" type is that in general, only
scalars can be coerced to other types. For example, a scalar `null`
and a string `> null` have the same in-memory representation. If they
are treated identically, this precludes unambiguously converting an
optional string whose contents are "null". With the two disambiguated,
we can choose to convert `null` to the null object and `> null` to a
string of contents "null". This ambiguity does not necessary exist for
the standard boolean values `true` and `false`, but it does allow the
conversion to be more strict, and it will theoretically result in
documents that read more naturally.
The motivation behind exposing flow_list and flow_map is that it will
allow preserving document formatting round trip (well, this isn't
strictly true: single line explicit strings neither remember whether
they were line strings or space strings, and they don't remember if
they were indented. However, that is much less information to lose).
The following formulations will parse to the same indistinguishable
value:
key: > value
key:
> value
key: | value
key:
| value
I think that's okay. It's a lot easier to chose a canonical form for
this case than it is for a map/list without any hints regarding its
origin.
2023-09-19 00:14:29 -07:00
|
|
|
document.root = try Value.fromScalar(arena_alloc, str);
|
2023-09-17 23:09:26 -07:00
|
|
|
// this is a cheesy hack. If the document consists
|
|
|
|
// solely of a scalar, the finalizer will try to
|
|
|
|
// chop a line ending off of it, so we need to add
|
|
|
|
// a sacrificial padding character to avoid
|
|
|
|
// chopping off something that matters.
|
|
|
|
try document.root.string.append(' ');
|
2023-09-13 00:11:45 -07:00
|
|
|
state = .done;
|
|
|
|
},
|
2023-09-17 23:09:26 -07:00
|
|
|
.line_string, .space_string => |str| {
|
config: differentiate fields in Value
This makes handling Value very slightly more work, but it provides
useful metadata that can be used to perform better conversion and
serialization.
The motivation behind the "scalar" type is that in general, only
scalars can be coerced to other types. For example, a scalar `null`
and a string `> null` have the same in-memory representation. If they
are treated identically, this precludes unambiguously converting an
optional string whose contents are "null". With the two disambiguated,
we can choose to convert `null` to the null object and `> null` to a
string of contents "null". This ambiguity does not necessary exist for
the standard boolean values `true` and `false`, but it does allow the
conversion to be more strict, and it will theoretically result in
documents that read more naturally.
The motivation behind exposing flow_list and flow_map is that it will
allow preserving document formatting round trip (well, this isn't
strictly true: single line explicit strings neither remember whether
they were line strings or space strings, and they don't remember if
they were indented. However, that is much less information to lose).
The following formulations will parse to the same indistinguishable
value:
key: > value
key:
> value
key: | value
key:
| value
I think that's okay. It's a lot easier to chose a canonical form for
this case than it is for a map/list without any hints regarding its
origin.
2023-09-19 00:14:29 -07:00
|
|
|
document.root = try Value.fromString(arena_alloc, str);
|
2023-09-17 23:09:26 -07:00
|
|
|
try document.root.string.append(in_line.lineEnding());
|
2023-09-14 23:38:24 -07:00
|
|
|
try stack.append(&document.root);
|
|
|
|
state = .value;
|
|
|
|
},
|
2023-09-13 00:11:45 -07:00
|
|
|
.flow_list => |str| {
|
2023-09-23 13:29:49 -07:00
|
|
|
document.root = try parseFlow(arena_alloc, str, .flow_list, self.dupe_behavior);
|
2023-09-13 00:11:45 -07:00
|
|
|
state = .done;
|
|
|
|
},
|
|
|
|
.flow_map => |str| {
|
2023-09-23 13:29:49 -07:00
|
|
|
document.root = try parseFlow(arena_alloc, str, .flow_map, self.dupe_behavior);
|
2023-09-13 00:11:45 -07:00
|
|
|
state = .done;
|
|
|
|
},
|
|
|
|
},
|
|
|
|
.list_item => |value| {
|
config: differentiate fields in Value
This makes handling Value very slightly more work, but it provides
useful metadata that can be used to perform better conversion and
serialization.
The motivation behind the "scalar" type is that in general, only
scalars can be coerced to other types. For example, a scalar `null`
and a string `> null` have the same in-memory representation. If they
are treated identically, this precludes unambiguously converting an
optional string whose contents are "null". With the two disambiguated,
we can choose to convert `null` to the null object and `> null` to a
string of contents "null". This ambiguity does not necessary exist for
the standard boolean values `true` and `false`, but it does allow the
conversion to be more strict, and it will theoretically result in
documents that read more naturally.
The motivation behind exposing flow_list and flow_map is that it will
allow preserving document formatting round trip (well, this isn't
strictly true: single line explicit strings neither remember whether
they were line strings or space strings, and they don't remember if
they were indented. However, that is much less information to lose).
The following formulations will parse to the same indistinguishable
value:
key: > value
key:
> value
key: | value
key:
| value
I think that's okay. It's a lot easier to chose a canonical form for
this case than it is for a map/list without any hints regarding its
origin.
2023-09-19 00:14:29 -07:00
|
|
|
document.root = Value.newList(arena_alloc);
|
2023-09-13 00:11:45 -07:00
|
|
|
try stack.append(&document.root);
|
2023-09-23 14:17:31 -07:00
|
|
|
state = .value;
|
2023-09-13 00:11:45 -07:00
|
|
|
|
|
|
|
switch (value) {
|
2023-09-23 14:17:31 -07:00
|
|
|
.empty => expect_shift = .indent,
|
|
|
|
.scalar => |str| try document.root.list.append(try Value.fromScalar(arena_alloc, str)),
|
|
|
|
.line_string, .space_string => |str| try document.root.list.append(try Value.fromString(arena_alloc, str)),
|
|
|
|
.flow_list => |str| try document.root.list.append(try parseFlow(arena_alloc, str, .flow_list, self.dupe_behavior)),
|
|
|
|
.flow_map => |str| try document.root.list.append(try parseFlow(arena_alloc, str, .flow_map, self.dupe_behavior)),
|
2023-09-13 00:11:45 -07:00
|
|
|
}
|
|
|
|
},
|
|
|
|
.map_item => |pair| {
|
config: differentiate fields in Value
This makes handling Value very slightly more work, but it provides
useful metadata that can be used to perform better conversion and
serialization.
The motivation behind the "scalar" type is that in general, only
scalars can be coerced to other types. For example, a scalar `null`
and a string `> null` have the same in-memory representation. If they
are treated identically, this precludes unambiguously converting an
optional string whose contents are "null". With the two disambiguated,
we can choose to convert `null` to the null object and `> null` to a
string of contents "null". This ambiguity does not necessary exist for
the standard boolean values `true` and `false`, but it does allow the
conversion to be more strict, and it will theoretically result in
documents that read more naturally.
The motivation behind exposing flow_list and flow_map is that it will
allow preserving document formatting round trip (well, this isn't
strictly true: single line explicit strings neither remember whether
they were line strings or space strings, and they don't remember if
they were indented. However, that is much less information to lose).
The following formulations will parse to the same indistinguishable
value:
key: > value
key:
> value
key: | value
key:
| value
I think that's okay. It's a lot easier to chose a canonical form for
this case than it is for a map/list without any hints regarding its
origin.
2023-09-19 00:14:29 -07:00
|
|
|
document.root = Value.newMap(arena_alloc);
|
2023-09-13 00:11:45 -07:00
|
|
|
try stack.append(&document.root);
|
2023-09-23 14:17:31 -07:00
|
|
|
state = .value;
|
2023-09-13 00:11:45 -07:00
|
|
|
|
2023-09-23 14:17:31 -07:00
|
|
|
const dupekey = try arena_alloc.dupe(u8, pair.key);
|
2023-09-13 00:11:45 -07:00
|
|
|
switch (pair.val) {
|
|
|
|
.empty => {
|
|
|
|
expect_shift = .indent;
|
|
|
|
// If the key is on its own line, we don't have
|
|
|
|
// an associated value until we parse the next
|
|
|
|
// line. We need to store a reference to this
|
|
|
|
// key somewhere until we can consume the
|
|
|
|
// value. More parser state to lug along.
|
|
|
|
|
2023-09-23 14:17:31 -07:00
|
|
|
dangling_key = dupekey;
|
2023-09-13 00:11:45 -07:00
|
|
|
},
|
2023-09-23 14:17:31 -07:00
|
|
|
.scalar => |str| try document.root.map.put(dupekey, try Value.fromScalar(arena_alloc, str)),
|
|
|
|
.line_string, .space_string => |str| try document.root.map.put(dupekey, try Value.fromString(arena_alloc, str)),
|
|
|
|
.flow_list => |str| try document.root.map.put(dupekey, try parseFlow(arena_alloc, str, .flow_list, self.dupe_behavior)),
|
|
|
|
.flow_map => |str| try document.root.map.put(dupekey, try parseFlow(arena_alloc, str, .flow_map, self.dupe_behavior)),
|
2023-09-13 00:11:45 -07:00
|
|
|
}
|
|
|
|
},
|
|
|
|
}
|
|
|
|
},
|
2023-09-14 23:38:24 -07:00
|
|
|
.value => switch (stack.getLast().*) {
|
config: differentiate fields in Value
This makes handling Value very slightly more work, but it provides
useful metadata that can be used to perform better conversion and
serialization.
The motivation behind the "scalar" type is that in general, only
scalars can be coerced to other types. For example, a scalar `null`
and a string `> null` have the same in-memory representation. If they
are treated identically, this precludes unambiguously converting an
optional string whose contents are "null". With the two disambiguated,
we can choose to convert `null` to the null object and `> null` to a
string of contents "null". This ambiguity does not necessary exist for
the standard boolean values `true` and `false`, but it does allow the
conversion to be more strict, and it will theoretically result in
documents that read more naturally.
The motivation behind exposing flow_list and flow_map is that it will
allow preserving document formatting round trip (well, this isn't
strictly true: single line explicit strings neither remember whether
they were line strings or space strings, and they don't remember if
they were indented. However, that is much less information to lose).
The following formulations will parse to the same indistinguishable
value:
key: > value
key:
> value
key: | value
key:
| value
I think that's okay. It's a lot easier to chose a canonical form for
this case than it is for a map/list without any hints regarding its
origin.
2023-09-19 00:14:29 -07:00
|
|
|
// these three states are never reachable here. flow_list and
|
|
|
|
// flow_map are parsed with a separate state machine. These
|
2023-09-23 01:07:04 -07:00
|
|
|
// value types can only be present by themselves as the first
|
config: differentiate fields in Value
This makes handling Value very slightly more work, but it provides
useful metadata that can be used to perform better conversion and
serialization.
The motivation behind the "scalar" type is that in general, only
scalars can be coerced to other types. For example, a scalar `null`
and a string `> null` have the same in-memory representation. If they
are treated identically, this precludes unambiguously converting an
optional string whose contents are "null". With the two disambiguated,
we can choose to convert `null` to the null object and `> null` to a
string of contents "null". This ambiguity does not necessary exist for
the standard boolean values `true` and `false`, but it does allow the
conversion to be more strict, and it will theoretically result in
documents that read more naturally.
The motivation behind exposing flow_list and flow_map is that it will
allow preserving document formatting round trip (well, this isn't
strictly true: single line explicit strings neither remember whether
they were line strings or space strings, and they don't remember if
they were indented. However, that is much less information to lose).
The following formulations will parse to the same indistinguishable
value:
key: > value
key:
> value
key: | value
key:
| value
I think that's okay. It's a lot easier to chose a canonical form for
this case than it is for a map/list without any hints regarding its
origin.
2023-09-19 00:14:29 -07:00
|
|
|
// line of the document, in which case the document consists
|
|
|
|
// only of that single line: this parser jumps immediately into
|
|
|
|
// the .done state, bypassing the .value state in which this
|
|
|
|
// switch is embedded.
|
|
|
|
.scalar, .flow_list, .flow_map => unreachable,
|
2023-09-13 00:11:45 -07:00
|
|
|
.string => |*string| {
|
2023-09-24 18:22:12 -07:00
|
|
|
if (line.shift == .indent)
|
2023-09-17 23:09:26 -07:00
|
|
|
return error.UnexpectedIndent;
|
|
|
|
|
2023-09-24 18:22:12 -07:00
|
|
|
if (!flop and line.shift == .dedent) {
|
2023-09-17 23:09:26 -07:00
|
|
|
// kick off the last trailing space or newline
|
2023-09-17 19:28:07 -07:00
|
|
|
_ = string.pop();
|
2023-09-13 00:11:45 -07:00
|
|
|
|
2023-09-24 18:22:12 -07:00
|
|
|
var dedent_depth = line.shift.dedent;
|
2023-09-13 00:11:45 -07:00
|
|
|
while (dedent_depth > 0) : (dedent_depth -= 1)
|
|
|
|
_ = stack.pop();
|
|
|
|
|
|
|
|
continue :flipflop;
|
|
|
|
}
|
|
|
|
|
|
|
|
switch (line.contents) {
|
|
|
|
.comment => unreachable,
|
|
|
|
.in_line => |in_line| switch (in_line) {
|
|
|
|
.empty => unreachable,
|
2023-09-17 23:09:26 -07:00
|
|
|
.line_string, .space_string => |str| {
|
2023-09-14 23:38:24 -07:00
|
|
|
try string.appendSlice(str);
|
2023-09-17 23:09:26 -07:00
|
|
|
try string.append(in_line.lineEnding());
|
2023-09-14 23:38:24 -07:00
|
|
|
},
|
2023-09-13 00:11:45 -07:00
|
|
|
else => return error.UnexpectedValue,
|
|
|
|
},
|
|
|
|
else => return error.UnexpectedValue,
|
|
|
|
}
|
|
|
|
},
|
|
|
|
.list => |*list| {
|
2023-09-17 23:09:26 -07:00
|
|
|
// detect that the previous item was actually empty
|
|
|
|
//
|
|
|
|
// -
|
|
|
|
// - something
|
|
|
|
//
|
|
|
|
// the first line here creates the expect_shift, but the second line
|
|
|
|
// is a valid continuation of the list despite not being indented
|
2023-09-24 18:22:12 -07:00
|
|
|
if (!flop and (expect_shift == .indent and line.shift != .indent))
|
config: differentiate fields in Value
This makes handling Value very slightly more work, but it provides
useful metadata that can be used to perform better conversion and
serialization.
The motivation behind the "scalar" type is that in general, only
scalars can be coerced to other types. For example, a scalar `null`
and a string `> null` have the same in-memory representation. If they
are treated identically, this precludes unambiguously converting an
optional string whose contents are "null". With the two disambiguated,
we can choose to convert `null` to the null object and `> null` to a
string of contents "null". This ambiguity does not necessary exist for
the standard boolean values `true` and `false`, but it does allow the
conversion to be more strict, and it will theoretically result in
documents that read more naturally.
The motivation behind exposing flow_list and flow_map is that it will
allow preserving document formatting round trip (well, this isn't
strictly true: single line explicit strings neither remember whether
they were line strings or space strings, and they don't remember if
they were indented. However, that is much less information to lose).
The following formulations will parse to the same indistinguishable
value:
key: > value
key:
> value
key: | value
key:
| value
I think that's okay. It's a lot easier to chose a canonical form for
this case than it is for a map/list without any hints regarding its
origin.
2023-09-19 00:14:29 -07:00
|
|
|
try list.append(Value.newScalar(arena_alloc));
|
2023-09-14 23:38:24 -07:00
|
|
|
|
2023-09-13 00:11:45 -07:00
|
|
|
// Consider:
|
|
|
|
//
|
2023-09-17 23:09:26 -07:00
|
|
|
// -
|
|
|
|
// own-line scalar
|
|
|
|
// - inline scalar
|
2023-09-13 00:11:45 -07:00
|
|
|
//
|
|
|
|
// the own-line scalar will not push the stack but the next list item will be a dedent
|
2023-09-24 18:22:12 -07:00
|
|
|
if (!flop and line.shift == .dedent) {
|
|
|
|
// if line.shift.dedent is 1 and we're expecting it, the stack will not be popped,
|
2023-09-13 00:11:45 -07:00
|
|
|
// but we will continue loop flipflop. However, flop will be set to false on the next
|
|
|
|
// trip, so this if prong will not be run again.
|
2023-09-24 18:22:12 -07:00
|
|
|
var dedent_depth = line.shift.dedent - @intFromBool(expect_shift == .dedent);
|
2023-09-13 00:11:45 -07:00
|
|
|
|
|
|
|
while (dedent_depth > 0) : (dedent_depth -= 1)
|
|
|
|
_ = stack.pop();
|
|
|
|
|
|
|
|
continue :flipflop;
|
|
|
|
}
|
|
|
|
|
|
|
|
switch (line.contents) {
|
|
|
|
.comment => unreachable,
|
|
|
|
.in_line => |in_line| {
|
|
|
|
// assert that this line has been indented. this is required for an inline value when
|
|
|
|
// the stack is in list mode.
|
2023-09-24 18:22:12 -07:00
|
|
|
if (expect_shift != .indent or line.shift != .indent)
|
2023-09-14 23:38:24 -07:00
|
|
|
return error.UnexpectedValue;
|
2023-09-13 00:11:45 -07:00
|
|
|
|
2023-09-14 23:38:24 -07:00
|
|
|
expect_shift = .dedent;
|
2023-09-13 00:11:45 -07:00
|
|
|
switch (in_line) {
|
|
|
|
.empty => unreachable,
|
config: differentiate fields in Value
This makes handling Value very slightly more work, but it provides
useful metadata that can be used to perform better conversion and
serialization.
The motivation behind the "scalar" type is that in general, only
scalars can be coerced to other types. For example, a scalar `null`
and a string `> null` have the same in-memory representation. If they
are treated identically, this precludes unambiguously converting an
optional string whose contents are "null". With the two disambiguated,
we can choose to convert `null` to the null object and `> null` to a
string of contents "null". This ambiguity does not necessary exist for
the standard boolean values `true` and `false`, but it does allow the
conversion to be more strict, and it will theoretically result in
documents that read more naturally.
The motivation behind exposing flow_list and flow_map is that it will
allow preserving document formatting round trip (well, this isn't
strictly true: single line explicit strings neither remember whether
they were line strings or space strings, and they don't remember if
they were indented. However, that is much less information to lose).
The following formulations will parse to the same indistinguishable
value:
key: > value
key:
> value
key: | value
key:
| value
I think that's okay. It's a lot easier to chose a canonical form for
this case than it is for a map/list without any hints regarding its
origin.
2023-09-19 00:14:29 -07:00
|
|
|
.scalar => |str| try list.append(try Value.fromScalar(arena_alloc, str)),
|
2023-09-23 13:29:49 -07:00
|
|
|
.flow_list => |str| try list.append(try parseFlow(arena_alloc, str, .flow_list, self.dupe_behavior)),
|
|
|
|
.flow_map => |str| try list.append(try parseFlow(arena_alloc, str, .flow_map, self.dupe_behavior)),
|
2023-09-17 23:09:26 -07:00
|
|
|
.line_string, .space_string => |str| {
|
2023-09-13 00:11:45 -07:00
|
|
|
// string pushes the stack
|
config: differentiate fields in Value
This makes handling Value very slightly more work, but it provides
useful metadata that can be used to perform better conversion and
serialization.
The motivation behind the "scalar" type is that in general, only
scalars can be coerced to other types. For example, a scalar `null`
and a string `> null` have the same in-memory representation. If they
are treated identically, this precludes unambiguously converting an
optional string whose contents are "null". With the two disambiguated,
we can choose to convert `null` to the null object and `> null` to a
string of contents "null". This ambiguity does not necessary exist for
the standard boolean values `true` and `false`, but it does allow the
conversion to be more strict, and it will theoretically result in
documents that read more naturally.
The motivation behind exposing flow_list and flow_map is that it will
allow preserving document formatting round trip (well, this isn't
strictly true: single line explicit strings neither remember whether
they were line strings or space strings, and they don't remember if
they were indented. However, that is much less information to lose).
The following formulations will parse to the same indistinguishable
value:
key: > value
key:
> value
key: | value
key:
| value
I think that's okay. It's a lot easier to chose a canonical form for
this case than it is for a map/list without any hints regarding its
origin.
2023-09-19 00:14:29 -07:00
|
|
|
const new_string = try appendListGetValue(list, try Value.fromString(arena_alloc, str));
|
2023-09-23 01:07:04 -07:00
|
|
|
try stack.append(new_string);
|
2023-09-14 23:38:24 -07:00
|
|
|
|
2023-09-17 23:09:26 -07:00
|
|
|
try new_string.string.append(in_line.lineEnding());
|
2023-09-13 00:11:45 -07:00
|
|
|
expect_shift = .none;
|
|
|
|
},
|
2023-09-14 23:38:24 -07:00
|
|
|
}
|
|
|
|
},
|
|
|
|
.list_item => |value| {
|
2023-09-24 18:22:12 -07:00
|
|
|
if (flop or (line.shift == .none or line.shift == .dedent)) {
|
2023-09-23 01:07:04 -07:00
|
|
|
expect_shift = .none;
|
|
|
|
switch (value) {
|
|
|
|
.empty => expect_shift = .indent,
|
|
|
|
.scalar => |str| try list.append(try Value.fromScalar(arena_alloc, str)),
|
|
|
|
.line_string, .space_string => |str| try list.append(try Value.fromString(arena_alloc, str)),
|
2023-09-23 13:29:49 -07:00
|
|
|
.flow_list => |str| try list.append(try parseFlow(arena_alloc, str, .flow_list, self.dupe_behavior)),
|
|
|
|
.flow_map => |str| try list.append(try parseFlow(arena_alloc, str, .flow_map, self.dupe_behavior)),
|
2023-09-23 01:07:04 -07:00
|
|
|
}
|
2023-09-24 18:22:12 -07:00
|
|
|
} else if (line.shift == .indent) {
|
2023-09-23 01:07:04 -07:00
|
|
|
if (expect_shift != .indent) return error.UnexpectedIndent;
|
|
|
|
|
|
|
|
const new_list = try appendListGetValue(list, Value.newList(arena_alloc));
|
|
|
|
try stack.append(new_list);
|
|
|
|
expect_shift = .none;
|
|
|
|
continue :flipflop;
|
|
|
|
} else unreachable;
|
2023-09-13 00:11:45 -07:00
|
|
|
},
|
2023-09-23 01:07:04 -07:00
|
|
|
.map_item => {
|
2023-09-14 23:38:24 -07:00
|
|
|
// this prong cannot be hit on dedent in a valid way.
|
|
|
|
//
|
|
|
|
// -
|
|
|
|
// map: value
|
|
|
|
// second: value
|
|
|
|
// third: value
|
|
|
|
//
|
|
|
|
// dedenting back to the list stack level requires list_item
|
|
|
|
|
2023-09-24 18:22:12 -07:00
|
|
|
if (line.shift != .indent)
|
2023-09-14 23:38:24 -07:00
|
|
|
return error.UnexpectedValue;
|
|
|
|
|
config: differentiate fields in Value
This makes handling Value very slightly more work, but it provides
useful metadata that can be used to perform better conversion and
serialization.
The motivation behind the "scalar" type is that in general, only
scalars can be coerced to other types. For example, a scalar `null`
and a string `> null` have the same in-memory representation. If they
are treated identically, this precludes unambiguously converting an
optional string whose contents are "null". With the two disambiguated,
we can choose to convert `null` to the null object and `> null` to a
string of contents "null". This ambiguity does not necessary exist for
the standard boolean values `true` and `false`, but it does allow the
conversion to be more strict, and it will theoretically result in
documents that read more naturally.
The motivation behind exposing flow_list and flow_map is that it will
allow preserving document formatting round trip (well, this isn't
strictly true: single line explicit strings neither remember whether
they were line strings or space strings, and they don't remember if
they were indented. However, that is much less information to lose).
The following formulations will parse to the same indistinguishable
value:
key: > value
key:
> value
key: | value
key:
| value
I think that's okay. It's a lot easier to chose a canonical form for
this case than it is for a map/list without any hints regarding its
origin.
2023-09-19 00:14:29 -07:00
|
|
|
const new_map = try appendListGetValue(list, Value.newMap(arena_alloc));
|
2023-09-14 23:38:24 -07:00
|
|
|
try stack.append(new_map);
|
|
|
|
expect_shift = .none;
|
2023-09-23 01:07:04 -07:00
|
|
|
continue :flipflop;
|
2023-09-13 00:11:45 -07:00
|
|
|
},
|
|
|
|
}
|
|
|
|
},
|
2023-09-14 23:38:24 -07:00
|
|
|
.map => |*map| {
|
2023-09-17 23:09:26 -07:00
|
|
|
// detect that the previous item was actually empty
|
|
|
|
//
|
|
|
|
// foo:
|
|
|
|
// bar: baz
|
|
|
|
//
|
|
|
|
// the first line here creates the expect_shift, but the second line
|
|
|
|
// is a valid continuation of the map despite not being indented
|
2023-09-24 18:22:12 -07:00
|
|
|
if (!flop and (expect_shift == .indent and line.shift != .indent)) {
|
2023-09-17 19:28:07 -07:00
|
|
|
try putMap(
|
2023-09-14 23:38:24 -07:00
|
|
|
map,
|
2023-09-17 19:28:07 -07:00
|
|
|
dangling_key orelse return error.Fail,
|
config: differentiate fields in Value
This makes handling Value very slightly more work, but it provides
useful metadata that can be used to perform better conversion and
serialization.
The motivation behind the "scalar" type is that in general, only
scalars can be coerced to other types. For example, a scalar `null`
and a string `> null` have the same in-memory representation. If they
are treated identically, this precludes unambiguously converting an
optional string whose contents are "null". With the two disambiguated,
we can choose to convert `null` to the null object and `> null` to a
string of contents "null". This ambiguity does not necessary exist for
the standard boolean values `true` and `false`, but it does allow the
conversion to be more strict, and it will theoretically result in
documents that read more naturally.
The motivation behind exposing flow_list and flow_map is that it will
allow preserving document formatting round trip (well, this isn't
strictly true: single line explicit strings neither remember whether
they were line strings or space strings, and they don't remember if
they were indented. However, that is much less information to lose).
The following formulations will parse to the same indistinguishable
value:
key: > value
key:
> value
key: | value
key:
| value
I think that's okay. It's a lot easier to chose a canonical form for
this case than it is for a map/list without any hints regarding its
origin.
2023-09-19 00:14:29 -07:00
|
|
|
Value.newScalar(arena_alloc),
|
2023-09-17 19:28:07 -07:00
|
|
|
self.dupe_behavior,
|
2023-09-14 23:38:24 -07:00
|
|
|
);
|
2023-09-17 19:28:07 -07:00
|
|
|
dangling_key = null;
|
2023-09-14 23:38:24 -07:00
|
|
|
}
|
|
|
|
|
2023-09-24 18:22:12 -07:00
|
|
|
if (!flop and line.shift == .dedent) {
|
|
|
|
var dedent_depth = line.shift.dedent - @intFromBool(expect_shift == .dedent);
|
2023-09-14 23:38:24 -07:00
|
|
|
|
|
|
|
while (dedent_depth > 0) : (dedent_depth -= 1)
|
|
|
|
_ = stack.pop();
|
|
|
|
|
2023-09-13 00:11:45 -07:00
|
|
|
continue :flipflop;
|
|
|
|
}
|
2023-09-14 23:38:24 -07:00
|
|
|
|
|
|
|
switch (line.contents) {
|
|
|
|
.comment => unreachable,
|
|
|
|
.in_line => |in_line| {
|
|
|
|
// assert that this line has been indented. this is required for an inline value when
|
|
|
|
// the stack is in map mode.
|
2023-09-24 18:22:12 -07:00
|
|
|
if (expect_shift != .indent or line.shift != .indent or dangling_key == null)
|
2023-09-14 23:38:24 -07:00
|
|
|
return error.UnexpectedValue;
|
|
|
|
|
|
|
|
expect_shift = .dedent;
|
|
|
|
|
|
|
|
switch (in_line) {
|
|
|
|
.empty => unreachable,
|
config: differentiate fields in Value
This makes handling Value very slightly more work, but it provides
useful metadata that can be used to perform better conversion and
serialization.
The motivation behind the "scalar" type is that in general, only
scalars can be coerced to other types. For example, a scalar `null`
and a string `> null` have the same in-memory representation. If they
are treated identically, this precludes unambiguously converting an
optional string whose contents are "null". With the two disambiguated,
we can choose to convert `null` to the null object and `> null` to a
string of contents "null". This ambiguity does not necessary exist for
the standard boolean values `true` and `false`, but it does allow the
conversion to be more strict, and it will theoretically result in
documents that read more naturally.
The motivation behind exposing flow_list and flow_map is that it will
allow preserving document formatting round trip (well, this isn't
strictly true: single line explicit strings neither remember whether
they were line strings or space strings, and they don't remember if
they were indented. However, that is much less information to lose).
The following formulations will parse to the same indistinguishable
value:
key: > value
key:
> value
key: | value
key:
| value
I think that's okay. It's a lot easier to chose a canonical form for
this case than it is for a map/list without any hints regarding its
origin.
2023-09-19 00:14:29 -07:00
|
|
|
.scalar => |str| try putMap(map, dangling_key.?, try Value.fromScalar(arena_alloc, str), self.dupe_behavior),
|
2023-09-23 13:29:49 -07:00
|
|
|
.flow_list => |str| try putMap(map, dangling_key.?, try parseFlow(arena_alloc, str, .flow_list, self.dupe_behavior), self.dupe_behavior),
|
2023-09-14 23:38:24 -07:00
|
|
|
.flow_map => |str| {
|
2023-09-23 13:29:49 -07:00
|
|
|
try putMap(map, dangling_key.?, try parseFlow(arena_alloc, str, .flow_map, self.dupe_behavior), self.dupe_behavior);
|
2023-09-14 23:38:24 -07:00
|
|
|
},
|
2023-09-17 23:09:26 -07:00
|
|
|
.line_string, .space_string => |str| {
|
2023-09-14 23:38:24 -07:00
|
|
|
// string pushes the stack
|
config: differentiate fields in Value
This makes handling Value very slightly more work, but it provides
useful metadata that can be used to perform better conversion and
serialization.
The motivation behind the "scalar" type is that in general, only
scalars can be coerced to other types. For example, a scalar `null`
and a string `> null` have the same in-memory representation. If they
are treated identically, this precludes unambiguously converting an
optional string whose contents are "null". With the two disambiguated,
we can choose to convert `null` to the null object and `> null` to a
string of contents "null". This ambiguity does not necessary exist for
the standard boolean values `true` and `false`, but it does allow the
conversion to be more strict, and it will theoretically result in
documents that read more naturally.
The motivation behind exposing flow_list and flow_map is that it will
allow preserving document formatting round trip (well, this isn't
strictly true: single line explicit strings neither remember whether
they were line strings or space strings, and they don't remember if
they were indented. However, that is much less information to lose).
The following formulations will parse to the same indistinguishable
value:
key: > value
key:
> value
key: | value
key:
| value
I think that's okay. It's a lot easier to chose a canonical form for
this case than it is for a map/list without any hints regarding its
origin.
2023-09-19 00:14:29 -07:00
|
|
|
const new_string = try putMapGetValue(map, dangling_key.?, try Value.fromString(arena_alloc, str), self.dupe_behavior);
|
2023-09-17 23:09:26 -07:00
|
|
|
try new_string.string.append(in_line.lineEnding());
|
2023-09-14 23:38:24 -07:00
|
|
|
try stack.append(new_string);
|
|
|
|
expect_shift = .none;
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
2023-09-17 19:28:07 -07:00
|
|
|
dangling_key = null;
|
2023-09-14 23:38:24 -07:00
|
|
|
},
|
2023-09-23 01:07:04 -07:00
|
|
|
.list_item => {
|
2023-09-14 23:38:24 -07:00
|
|
|
// this prong cannot be hit on dedent in a valid way.
|
|
|
|
//
|
|
|
|
// map:
|
|
|
|
// - value
|
|
|
|
// - invalid
|
|
|
|
//
|
|
|
|
// dedenting back to the map stack level requires map_item
|
|
|
|
|
2023-09-24 18:22:12 -07:00
|
|
|
if (expect_shift != .indent or line.shift != .indent or dangling_key == null)
|
2023-09-14 23:38:24 -07:00
|
|
|
return error.UnexpectedValue;
|
|
|
|
|
config: differentiate fields in Value
This makes handling Value very slightly more work, but it provides
useful metadata that can be used to perform better conversion and
serialization.
The motivation behind the "scalar" type is that in general, only
scalars can be coerced to other types. For example, a scalar `null`
and a string `> null` have the same in-memory representation. If they
are treated identically, this precludes unambiguously converting an
optional string whose contents are "null". With the two disambiguated,
we can choose to convert `null` to the null object and `> null` to a
string of contents "null". This ambiguity does not necessary exist for
the standard boolean values `true` and `false`, but it does allow the
conversion to be more strict, and it will theoretically result in
documents that read more naturally.
The motivation behind exposing flow_list and flow_map is that it will
allow preserving document formatting round trip (well, this isn't
strictly true: single line explicit strings neither remember whether
they were line strings or space strings, and they don't remember if
they were indented. However, that is much less information to lose).
The following formulations will parse to the same indistinguishable
value:
key: > value
key:
> value
key: | value
key:
| value
I think that's okay. It's a lot easier to chose a canonical form for
this case than it is for a map/list without any hints regarding its
origin.
2023-09-19 00:14:29 -07:00
|
|
|
const new_list = try putMapGetValue(map, dangling_key.?, Value.newList(arena_alloc), self.dupe_behavior);
|
2023-09-14 23:38:24 -07:00
|
|
|
try stack.append(new_list);
|
2023-09-17 19:28:07 -07:00
|
|
|
dangling_key = null;
|
2023-09-14 23:38:24 -07:00
|
|
|
expect_shift = .none;
|
2023-09-23 01:07:04 -07:00
|
|
|
continue :flipflop;
|
2023-09-14 23:38:24 -07:00
|
|
|
},
|
|
|
|
.map_item => |pair| {
|
2023-09-24 18:22:12 -07:00
|
|
|
if (flop or (line.shift == .none or line.shift == .dedent)) {
|
2023-09-23 01:07:04 -07:00
|
|
|
expect_shift = .none;
|
2023-09-23 14:17:31 -07:00
|
|
|
const dupekey = try arena_alloc.dupe(u8, pair.key);
|
2023-09-23 01:07:04 -07:00
|
|
|
switch (pair.val) {
|
2023-09-14 23:38:24 -07:00
|
|
|
.empty => {
|
|
|
|
expect_shift = .indent;
|
2023-09-23 14:17:31 -07:00
|
|
|
dangling_key = dupekey;
|
2023-09-14 23:38:24 -07:00
|
|
|
},
|
2023-09-23 14:17:31 -07:00
|
|
|
.scalar => |str| try putMap(map, dupekey, try Value.fromScalar(arena_alloc, str), self.dupe_behavior),
|
|
|
|
.line_string, .space_string => |str| try putMap(map, dupekey, try Value.fromString(arena_alloc, str), self.dupe_behavior),
|
|
|
|
.flow_list => |str| try putMap(map, dupekey, try parseFlow(arena_alloc, str, .flow_list, self.dupe_behavior), self.dupe_behavior),
|
|
|
|
.flow_map => |str| try putMap(map, dupekey, try parseFlow(arena_alloc, str, .flow_map, self.dupe_behavior), self.dupe_behavior),
|
2023-09-23 01:07:04 -07:00
|
|
|
}
|
2023-09-24 18:22:12 -07:00
|
|
|
} else if (line.shift == .indent) {
|
2023-09-23 01:07:04 -07:00
|
|
|
if (expect_shift != .indent or dangling_key == null) return error.UnexpectedValue;
|
|
|
|
|
|
|
|
const new_map = try putMapGetValue(map, dangling_key.?, Value.newMap(arena_alloc), self.dupe_behavior);
|
|
|
|
try stack.append(new_map);
|
|
|
|
dangling_key = null;
|
|
|
|
continue :flipflop;
|
|
|
|
} else unreachable;
|
2023-09-14 23:38:24 -07:00
|
|
|
},
|
|
|
|
}
|
2023-09-13 00:11:45 -07:00
|
|
|
},
|
|
|
|
},
|
|
|
|
.done => return error.ExtraContent,
|
|
|
|
}
|
|
|
|
|
2023-09-14 23:38:24 -07:00
|
|
|
// this is specifically performed at the end of the loop body so that
|
|
|
|
// `continue :flipflop` skips setting it.
|
|
|
|
flip = false;
|
|
|
|
}
|
2023-09-13 00:11:45 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
switch (state) {
|
|
|
|
.initial => switch (self.default_object) {
|
2023-09-24 18:22:12 -07:00
|
|
|
.scalar => document.root = .{ .scalar = std.ArrayList(u8).init(arena_alloc) },
|
2023-09-13 00:11:45 -07:00
|
|
|
.string => document.root = .{ .string = std.ArrayList(u8).init(arena_alloc) },
|
config: differentiate fields in Value
This makes handling Value very slightly more work, but it provides
useful metadata that can be used to perform better conversion and
serialization.
The motivation behind the "scalar" type is that in general, only
scalars can be coerced to other types. For example, a scalar `null`
and a string `> null` have the same in-memory representation. If they
are treated identically, this precludes unambiguously converting an
optional string whose contents are "null". With the two disambiguated,
we can choose to convert `null` to the null object and `> null` to a
string of contents "null". This ambiguity does not necessary exist for
the standard boolean values `true` and `false`, but it does allow the
conversion to be more strict, and it will theoretically result in
documents that read more naturally.
The motivation behind exposing flow_list and flow_map is that it will
allow preserving document formatting round trip (well, this isn't
strictly true: single line explicit strings neither remember whether
they were line strings or space strings, and they don't remember if
they were indented. However, that is much less information to lose).
The following formulations will parse to the same indistinguishable
value:
key: > value
key:
> value
key: | value
key:
| value
I think that's okay. It's a lot easier to chose a canonical form for
this case than it is for a map/list without any hints regarding its
origin.
2023-09-19 00:14:29 -07:00
|
|
|
.list => document.root = Value.newList(arena_alloc),
|
|
|
|
.map => document.root = Value.newMap(arena_alloc),
|
2023-09-13 00:11:45 -07:00
|
|
|
.fail => return error.EmptyDocument,
|
|
|
|
},
|
2023-09-14 23:38:24 -07:00
|
|
|
.value => switch (stack.getLast().*) {
|
|
|
|
// remove the final trailing newline or space
|
config: differentiate fields in Value
This makes handling Value very slightly more work, but it provides
useful metadata that can be used to perform better conversion and
serialization.
The motivation behind the "scalar" type is that in general, only
scalars can be coerced to other types. For example, a scalar `null`
and a string `> null` have the same in-memory representation. If they
are treated identically, this precludes unambiguously converting an
optional string whose contents are "null". With the two disambiguated,
we can choose to convert `null` to the null object and `> null` to a
string of contents "null". This ambiguity does not necessary exist for
the standard boolean values `true` and `false`, but it does allow the
conversion to be more strict, and it will theoretically result in
documents that read more naturally.
The motivation behind exposing flow_list and flow_map is that it will
allow preserving document formatting round trip (well, this isn't
strictly true: single line explicit strings neither remember whether
they were line strings or space strings, and they don't remember if
they were indented. However, that is much less information to lose).
The following formulations will parse to the same indistinguishable
value:
key: > value
key:
> value
key: | value
key:
| value
I think that's okay. It's a lot easier to chose a canonical form for
this case than it is for a map/list without any hints regarding its
origin.
2023-09-19 00:14:29 -07:00
|
|
|
.scalar, .string => |*string| _ = string.popOrNull(),
|
2023-09-14 23:38:24 -07:00
|
|
|
// if we have a dangling -, attach an empty string to it
|
config: differentiate fields in Value
This makes handling Value very slightly more work, but it provides
useful metadata that can be used to perform better conversion and
serialization.
The motivation behind the "scalar" type is that in general, only
scalars can be coerced to other types. For example, a scalar `null`
and a string `> null` have the same in-memory representation. If they
are treated identically, this precludes unambiguously converting an
optional string whose contents are "null". With the two disambiguated,
we can choose to convert `null` to the null object and `> null` to a
string of contents "null". This ambiguity does not necessary exist for
the standard boolean values `true` and `false`, but it does allow the
conversion to be more strict, and it will theoretically result in
documents that read more naturally.
The motivation behind exposing flow_list and flow_map is that it will
allow preserving document formatting round trip (well, this isn't
strictly true: single line explicit strings neither remember whether
they were line strings or space strings, and they don't remember if
they were indented. However, that is much less information to lose).
The following formulations will parse to the same indistinguishable
value:
key: > value
key:
> value
key: | value
key:
| value
I think that's okay. It's a lot easier to chose a canonical form for
this case than it is for a map/list without any hints regarding its
origin.
2023-09-19 00:14:29 -07:00
|
|
|
.list => |*list| if (expect_shift == .indent) try list.append(Value.newScalar(arena_alloc)),
|
2023-09-17 19:28:07 -07:00
|
|
|
// if we have a dangling "key:", attach an empty string to it
|
config: differentiate fields in Value
This makes handling Value very slightly more work, but it provides
useful metadata that can be used to perform better conversion and
serialization.
The motivation behind the "scalar" type is that in general, only
scalars can be coerced to other types. For example, a scalar `null`
and a string `> null` have the same in-memory representation. If they
are treated identically, this precludes unambiguously converting an
optional string whose contents are "null". With the two disambiguated,
we can choose to convert `null` to the null object and `> null` to a
string of contents "null". This ambiguity does not necessary exist for
the standard boolean values `true` and `false`, but it does allow the
conversion to be more strict, and it will theoretically result in
documents that read more naturally.
The motivation behind exposing flow_list and flow_map is that it will
allow preserving document formatting round trip (well, this isn't
strictly true: single line explicit strings neither remember whether
they were line strings or space strings, and they don't remember if
they were indented. However, that is much less information to lose).
The following formulations will parse to the same indistinguishable
value:
key: > value
key:
> value
key: | value
key:
| value
I think that's okay. It's a lot easier to chose a canonical form for
this case than it is for a map/list without any hints regarding its
origin.
2023-09-19 00:14:29 -07:00
|
|
|
.map => |*map| if (dangling_key) |dk| try putMap(map, dk, Value.newScalar(arena_alloc), self.dupe_behavior),
|
|
|
|
.flow_list, .flow_map => {},
|
2023-09-13 00:11:45 -07:00
|
|
|
},
|
|
|
|
.done => {},
|
|
|
|
}
|
|
|
|
|
|
|
|
return document;
|
|
|
|
}
|
|
|
|
|
2023-09-23 17:27:21 -07:00
|
|
|
const FlowStack: type = std.ArrayList(*Value);
|
2023-09-17 19:47:18 -07:00
|
|
|
|
2023-09-23 17:27:21 -07:00
|
|
|
inline fn getStackTip(stack: FlowStack) Error!*Value {
|
2023-09-17 19:47:18 -07:00
|
|
|
if (stack.items.len == 0) return error.BadState;
|
2023-09-23 17:27:21 -07:00
|
|
|
return stack.items[stack.items.len - 1];
|
2023-09-17 19:47:18 -07:00
|
|
|
}
|
|
|
|
|
2023-09-23 13:29:49 -07:00
|
|
|
inline fn popStack(stack: *FlowStack) Error!FlowParseState {
|
|
|
|
if (stack.popOrNull() == null)
|
config: start doing some code cleanup
I was pretty sloppy with the code organization while writing out the
state machines because my focus was on thinking through the parsing
process and logic there. However, The code was not in good shape to
continue implementing code features (not document features). This is
the first of probably several commits that will work on cleaning up
some things.
Value has been promoted to the top level namespace, and Document has an
initializer function. Referencing Value.List and Value.Map are much
cleaner now. Type aliases are good.
For the flow parser, `popStack` does not have to access anything except
the current stack. This can be passed in as a parameter. This means
that `parse` is ready to be refactored to take a buffer and an
allocator.
The main next steps for code improvement are:
1. reentrant/streaming parser. I am planning to leave it as
line-buffered, though I could go further. Line-buffered has two main
benefits: the tokenizer doesn't need to be refactored significantly,
and the flow parser doesn't need to be made reentrant. I may
reevaluate this as I am implementing it, however, as those changes
may be simpler than I think.
2. Actually implement the error diagnostics info. I have some skeleton
structure in place for this, so it should just be doing the work of
getting it hooked up.
3. Parse into object. Metaprogramming, let's go. It will be interesting
to try to do this non-recursively, as well (curious to see if it
results in code bloat).
4. Object to Document. This is probably going to be annoying, since
there are a variety of edge cases that will have to be handled. And
lots of objects that cannot be represented as documents.
5. Serialize Document. One thing the parser does not preserve is
whether a Value was flow-style or not, so it will be impossible to
do round-trip formatting preservation. That's currently a non-goal,
and I haven't decided yet if flow-style output should be based on
some heuristic (number/length of values in container) or just never
emitted. Lack of round-trip preservation does make using this as a
general purpose config format a lot more dubious, so I will have to
think about this some more.
6. Document to JSON. Why not? I will hand roll this and it will suck.
And then everything will be perfect and never need to be touched again.
2023-09-18 00:01:36 -07:00
|
|
|
return error.BadState;
|
2023-09-17 19:47:18 -07:00
|
|
|
|
2023-09-23 13:29:49 -07:00
|
|
|
const parent = stack.getLastOrNull() orelse return .done;
|
2023-09-17 19:47:18 -07:00
|
|
|
|
2023-09-23 17:27:21 -07:00
|
|
|
return switch (parent.*) {
|
config: differentiate fields in Value
This makes handling Value very slightly more work, but it provides
useful metadata that can be used to perform better conversion and
serialization.
The motivation behind the "scalar" type is that in general, only
scalars can be coerced to other types. For example, a scalar `null`
and a string `> null` have the same in-memory representation. If they
are treated identically, this precludes unambiguously converting an
optional string whose contents are "null". With the two disambiguated,
we can choose to convert `null` to the null object and `> null` to a
string of contents "null". This ambiguity does not necessary exist for
the standard boolean values `true` and `false`, but it does allow the
conversion to be more strict, and it will theoretically result in
documents that read more naturally.
The motivation behind exposing flow_list and flow_map is that it will
allow preserving document formatting round trip (well, this isn't
strictly true: single line explicit strings neither remember whether
they were line strings or space strings, and they don't remember if
they were indented. However, that is much less information to lose).
The following formulations will parse to the same indistinguishable
value:
key: > value
key:
> value
key: | value
key:
| value
I think that's okay. It's a lot easier to chose a canonical form for
this case than it is for a map/list without any hints regarding its
origin.
2023-09-19 00:14:29 -07:00
|
|
|
.flow_list => .want_list_separator,
|
|
|
|
.flow_map => .want_map_separator,
|
2023-09-17 19:47:18 -07:00
|
|
|
else => return error.BadState,
|
config: start doing some code cleanup
I was pretty sloppy with the code organization while writing out the
state machines because my focus was on thinking through the parsing
process and logic there. However, The code was not in good shape to
continue implementing code features (not document features). This is
the first of probably several commits that will work on cleaning up
some things.
Value has been promoted to the top level namespace, and Document has an
initializer function. Referencing Value.List and Value.Map are much
cleaner now. Type aliases are good.
For the flow parser, `popStack` does not have to access anything except
the current stack. This can be passed in as a parameter. This means
that `parse` is ready to be refactored to take a buffer and an
allocator.
The main next steps for code improvement are:
1. reentrant/streaming parser. I am planning to leave it as
line-buffered, though I could go further. Line-buffered has two main
benefits: the tokenizer doesn't need to be refactored significantly,
and the flow parser doesn't need to be made reentrant. I may
reevaluate this as I am implementing it, however, as those changes
may be simpler than I think.
2. Actually implement the error diagnostics info. I have some skeleton
structure in place for this, so it should just be doing the work of
getting it hooked up.
3. Parse into object. Metaprogramming, let's go. It will be interesting
to try to do this non-recursively, as well (curious to see if it
results in code bloat).
4. Object to Document. This is probably going to be annoying, since
there are a variety of edge cases that will have to be handled. And
lots of objects that cannot be represented as documents.
5. Serialize Document. One thing the parser does not preserve is
whether a Value was flow-style or not, so it will be impossible to
do round-trip formatting preservation. That's currently a non-goal,
and I haven't decided yet if flow-style output should be based on
some heuristic (number/length of values in container) or just never
emitted. Lack of round-trip preservation does make using this as a
general purpose config format a lot more dubious, so I will have to
think about this some more.
6. Document to JSON. Why not? I will hand roll this and it will suck.
And then everything will be perfect and never need to be touched again.
2023-09-18 00:01:36 -07:00
|
|
|
};
|
2023-09-17 19:47:18 -07:00
|
|
|
}
|
|
|
|
|
2023-09-23 13:29:49 -07:00
|
|
|
const FlowParseState = enum {
|
|
|
|
want_list_item,
|
|
|
|
consuming_list_item,
|
|
|
|
want_list_separator,
|
|
|
|
want_map_key,
|
|
|
|
consuming_map_key,
|
|
|
|
want_map_value,
|
|
|
|
consuming_map_value,
|
|
|
|
want_map_separator,
|
|
|
|
done,
|
|
|
|
};
|
|
|
|
|
|
|
|
pub fn parseFlow(
|
|
|
|
alloc: std.mem.Allocator,
|
|
|
|
contents: []const u8,
|
|
|
|
root_type: Value.TagType,
|
|
|
|
dupe_behavior: DuplicateKeyBehavior,
|
|
|
|
) Error!Value {
|
|
|
|
var root: Value = switch (root_type) {
|
|
|
|
.flow_list => Value.newFlowList(alloc),
|
|
|
|
.flow_map => Value.newFlowMap(alloc),
|
|
|
|
else => return error.BadState,
|
|
|
|
};
|
|
|
|
var state: FlowParseState = switch (root_type) {
|
|
|
|
.flow_list => .want_list_item,
|
|
|
|
.flow_map => .want_map_key,
|
|
|
|
else => unreachable,
|
|
|
|
};
|
|
|
|
var stack = try FlowStack.initCapacity(alloc, 1);
|
2023-09-23 17:27:21 -07:00
|
|
|
stack.appendAssumeCapacity(&root);
|
|
|
|
// used to distinguish betwen [] and [ ], and it also tracks
|
|
|
|
// a continuous value between different states
|
|
|
|
var item_start: usize = 0;
|
2023-09-17 23:09:26 -07:00
|
|
|
var dangling_key: ?[]const u8 = null;
|
|
|
|
|
2023-09-23 13:29:49 -07:00
|
|
|
charloop: for (contents, 0..) |char, idx| {
|
|
|
|
switch (state) {
|
2023-09-17 19:47:18 -07:00
|
|
|
.want_list_item => switch (char) {
|
|
|
|
' ', '\t' => continue :charloop,
|
|
|
|
',' => {
|
|
|
|
// empty value
|
2023-09-23 13:29:49 -07:00
|
|
|
const tip = try getStackTip(stack);
|
2023-09-23 17:27:21 -07:00
|
|
|
try tip.flow_list.append(Value.newScalar(alloc));
|
|
|
|
item_start = idx + 1;
|
2023-09-17 19:47:18 -07:00
|
|
|
},
|
|
|
|
'{' => {
|
2023-09-23 13:29:49 -07:00
|
|
|
const tip = try getStackTip(stack);
|
2023-09-17 19:47:18 -07:00
|
|
|
|
|
|
|
const new_map = try Parser.appendListGetValue(
|
2023-09-23 17:27:21 -07:00
|
|
|
&tip.flow_list,
|
2023-09-23 13:29:49 -07:00
|
|
|
Value.newFlowMap(alloc),
|
2023-09-17 19:47:18 -07:00
|
|
|
);
|
|
|
|
|
2023-09-23 17:27:21 -07:00
|
|
|
item_start = idx;
|
|
|
|
try stack.append(new_map);
|
2023-09-23 13:29:49 -07:00
|
|
|
state = .want_map_key;
|
2023-09-17 19:47:18 -07:00
|
|
|
},
|
|
|
|
'[' => {
|
2023-09-23 13:29:49 -07:00
|
|
|
const tip = try getStackTip(stack);
|
2023-09-17 19:47:18 -07:00
|
|
|
|
|
|
|
const new_list = try Parser.appendListGetValue(
|
2023-09-23 17:27:21 -07:00
|
|
|
&tip.flow_list,
|
2023-09-23 13:29:49 -07:00
|
|
|
Value.newFlowList(alloc),
|
2023-09-17 19:47:18 -07:00
|
|
|
);
|
|
|
|
|
2023-09-23 17:27:21 -07:00
|
|
|
item_start = idx + 1;
|
|
|
|
try stack.append(new_list);
|
2023-09-23 13:29:49 -07:00
|
|
|
state = .want_list_item;
|
2023-09-17 19:47:18 -07:00
|
|
|
},
|
config: start doing some code cleanup
I was pretty sloppy with the code organization while writing out the
state machines because my focus was on thinking through the parsing
process and logic there. However, The code was not in good shape to
continue implementing code features (not document features). This is
the first of probably several commits that will work on cleaning up
some things.
Value has been promoted to the top level namespace, and Document has an
initializer function. Referencing Value.List and Value.Map are much
cleaner now. Type aliases are good.
For the flow parser, `popStack` does not have to access anything except
the current stack. This can be passed in as a parameter. This means
that `parse` is ready to be refactored to take a buffer and an
allocator.
The main next steps for code improvement are:
1. reentrant/streaming parser. I am planning to leave it as
line-buffered, though I could go further. Line-buffered has two main
benefits: the tokenizer doesn't need to be refactored significantly,
and the flow parser doesn't need to be made reentrant. I may
reevaluate this as I am implementing it, however, as those changes
may be simpler than I think.
2. Actually implement the error diagnostics info. I have some skeleton
structure in place for this, so it should just be doing the work of
getting it hooked up.
3. Parse into object. Metaprogramming, let's go. It will be interesting
to try to do this non-recursively, as well (curious to see if it
results in code bloat).
4. Object to Document. This is probably going to be annoying, since
there are a variety of edge cases that will have to be handled. And
lots of objects that cannot be represented as documents.
5. Serialize Document. One thing the parser does not preserve is
whether a Value was flow-style or not, so it will be impossible to
do round-trip formatting preservation. That's currently a non-goal,
and I haven't decided yet if flow-style output should be based on
some heuristic (number/length of values in container) or just never
emitted. Lack of round-trip preservation does make using this as a
general purpose config format a lot more dubious, so I will have to
think about this some more.
6. Document to JSON. Why not? I will hand roll this and it will suck.
And then everything will be perfect and never need to be touched again.
2023-09-18 00:01:36 -07:00
|
|
|
']' => {
|
2023-09-23 13:29:49 -07:00
|
|
|
const finished = stack.getLastOrNull() orelse return error.BadState;
|
2023-09-23 17:27:21 -07:00
|
|
|
if (finished.flow_list.items.len > 0 or idx > item_start)
|
|
|
|
try finished.flow_list.append(Value.newScalar(alloc));
|
2023-09-23 13:29:49 -07:00
|
|
|
state = try popStack(&stack);
|
config: start doing some code cleanup
I was pretty sloppy with the code organization while writing out the
state machines because my focus was on thinking through the parsing
process and logic there. However, The code was not in good shape to
continue implementing code features (not document features). This is
the first of probably several commits that will work on cleaning up
some things.
Value has been promoted to the top level namespace, and Document has an
initializer function. Referencing Value.List and Value.Map are much
cleaner now. Type aliases are good.
For the flow parser, `popStack` does not have to access anything except
the current stack. This can be passed in as a parameter. This means
that `parse` is ready to be refactored to take a buffer and an
allocator.
The main next steps for code improvement are:
1. reentrant/streaming parser. I am planning to leave it as
line-buffered, though I could go further. Line-buffered has two main
benefits: the tokenizer doesn't need to be refactored significantly,
and the flow parser doesn't need to be made reentrant. I may
reevaluate this as I am implementing it, however, as those changes
may be simpler than I think.
2. Actually implement the error diagnostics info. I have some skeleton
structure in place for this, so it should just be doing the work of
getting it hooked up.
3. Parse into object. Metaprogramming, let's go. It will be interesting
to try to do this non-recursively, as well (curious to see if it
results in code bloat).
4. Object to Document. This is probably going to be annoying, since
there are a variety of edge cases that will have to be handled. And
lots of objects that cannot be represented as documents.
5. Serialize Document. One thing the parser does not preserve is
whether a Value was flow-style or not, so it will be impossible to
do round-trip formatting preservation. That's currently a non-goal,
and I haven't decided yet if flow-style output should be based on
some heuristic (number/length of values in container) or just never
emitted. Lack of round-trip preservation does make using this as a
general purpose config format a lot more dubious, so I will have to
think about this some more.
6. Document to JSON. Why not? I will hand roll this and it will suck.
And then everything will be perfect and never need to be touched again.
2023-09-18 00:01:36 -07:00
|
|
|
},
|
2023-09-17 19:47:18 -07:00
|
|
|
else => {
|
2023-09-23 17:27:21 -07:00
|
|
|
item_start = idx;
|
2023-09-23 13:29:49 -07:00
|
|
|
state = .consuming_list_item;
|
2023-09-17 19:47:18 -07:00
|
|
|
},
|
|
|
|
},
|
|
|
|
.consuming_list_item => switch (char) {
|
|
|
|
',' => {
|
2023-09-23 13:29:49 -07:00
|
|
|
const tip = try getStackTip(stack);
|
2023-09-17 19:47:18 -07:00
|
|
|
|
2023-09-23 17:27:21 -07:00
|
|
|
try tip.flow_list.append(
|
|
|
|
try Value.fromScalar(alloc, contents[item_start..idx]),
|
2023-09-17 19:47:18 -07:00
|
|
|
);
|
2023-09-23 17:27:21 -07:00
|
|
|
item_start = idx + 1;
|
2023-09-17 19:47:18 -07:00
|
|
|
|
2023-09-23 13:29:49 -07:00
|
|
|
state = .want_list_item;
|
2023-09-17 19:47:18 -07:00
|
|
|
},
|
config: start doing some code cleanup
I was pretty sloppy with the code organization while writing out the
state machines because my focus was on thinking through the parsing
process and logic there. However, The code was not in good shape to
continue implementing code features (not document features). This is
the first of probably several commits that will work on cleaning up
some things.
Value has been promoted to the top level namespace, and Document has an
initializer function. Referencing Value.List and Value.Map are much
cleaner now. Type aliases are good.
For the flow parser, `popStack` does not have to access anything except
the current stack. This can be passed in as a parameter. This means
that `parse` is ready to be refactored to take a buffer and an
allocator.
The main next steps for code improvement are:
1. reentrant/streaming parser. I am planning to leave it as
line-buffered, though I could go further. Line-buffered has two main
benefits: the tokenizer doesn't need to be refactored significantly,
and the flow parser doesn't need to be made reentrant. I may
reevaluate this as I am implementing it, however, as those changes
may be simpler than I think.
2. Actually implement the error diagnostics info. I have some skeleton
structure in place for this, so it should just be doing the work of
getting it hooked up.
3. Parse into object. Metaprogramming, let's go. It will be interesting
to try to do this non-recursively, as well (curious to see if it
results in code bloat).
4. Object to Document. This is probably going to be annoying, since
there are a variety of edge cases that will have to be handled. And
lots of objects that cannot be represented as documents.
5. Serialize Document. One thing the parser does not preserve is
whether a Value was flow-style or not, so it will be impossible to
do round-trip formatting preservation. That's currently a non-goal,
and I haven't decided yet if flow-style output should be based on
some heuristic (number/length of values in container) or just never
emitted. Lack of round-trip preservation does make using this as a
general purpose config format a lot more dubious, so I will have to
think about this some more.
6. Document to JSON. Why not? I will hand roll this and it will suck.
And then everything will be perfect and never need to be touched again.
2023-09-18 00:01:36 -07:00
|
|
|
']' => {
|
2023-09-23 13:29:49 -07:00
|
|
|
const finished = stack.getLastOrNull() orelse return error.BadState;
|
2023-09-23 17:27:21 -07:00
|
|
|
try finished.flow_list.append(
|
|
|
|
try Value.fromScalar(alloc, contents[item_start..idx]),
|
config: start doing some code cleanup
I was pretty sloppy with the code organization while writing out the
state machines because my focus was on thinking through the parsing
process and logic there. However, The code was not in good shape to
continue implementing code features (not document features). This is
the first of probably several commits that will work on cleaning up
some things.
Value has been promoted to the top level namespace, and Document has an
initializer function. Referencing Value.List and Value.Map are much
cleaner now. Type aliases are good.
For the flow parser, `popStack` does not have to access anything except
the current stack. This can be passed in as a parameter. This means
that `parse` is ready to be refactored to take a buffer and an
allocator.
The main next steps for code improvement are:
1. reentrant/streaming parser. I am planning to leave it as
line-buffered, though I could go further. Line-buffered has two main
benefits: the tokenizer doesn't need to be refactored significantly,
and the flow parser doesn't need to be made reentrant. I may
reevaluate this as I am implementing it, however, as those changes
may be simpler than I think.
2. Actually implement the error diagnostics info. I have some skeleton
structure in place for this, so it should just be doing the work of
getting it hooked up.
3. Parse into object. Metaprogramming, let's go. It will be interesting
to try to do this non-recursively, as well (curious to see if it
results in code bloat).
4. Object to Document. This is probably going to be annoying, since
there are a variety of edge cases that will have to be handled. And
lots of objects that cannot be represented as documents.
5. Serialize Document. One thing the parser does not preserve is
whether a Value was flow-style or not, so it will be impossible to
do round-trip formatting preservation. That's currently a non-goal,
and I haven't decided yet if flow-style output should be based on
some heuristic (number/length of values in container) or just never
emitted. Lack of round-trip preservation does make using this as a
general purpose config format a lot more dubious, so I will have to
think about this some more.
6. Document to JSON. Why not? I will hand roll this and it will suck.
And then everything will be perfect and never need to be touched again.
2023-09-18 00:01:36 -07:00
|
|
|
);
|
2023-09-23 13:29:49 -07:00
|
|
|
state = try popStack(&stack);
|
config: start doing some code cleanup
I was pretty sloppy with the code organization while writing out the
state machines because my focus was on thinking through the parsing
process and logic there. However, The code was not in good shape to
continue implementing code features (not document features). This is
the first of probably several commits that will work on cleaning up
some things.
Value has been promoted to the top level namespace, and Document has an
initializer function. Referencing Value.List and Value.Map are much
cleaner now. Type aliases are good.
For the flow parser, `popStack` does not have to access anything except
the current stack. This can be passed in as a parameter. This means
that `parse` is ready to be refactored to take a buffer and an
allocator.
The main next steps for code improvement are:
1. reentrant/streaming parser. I am planning to leave it as
line-buffered, though I could go further. Line-buffered has two main
benefits: the tokenizer doesn't need to be refactored significantly,
and the flow parser doesn't need to be made reentrant. I may
reevaluate this as I am implementing it, however, as those changes
may be simpler than I think.
2. Actually implement the error diagnostics info. I have some skeleton
structure in place for this, so it should just be doing the work of
getting it hooked up.
3. Parse into object. Metaprogramming, let's go. It will be interesting
to try to do this non-recursively, as well (curious to see if it
results in code bloat).
4. Object to Document. This is probably going to be annoying, since
there are a variety of edge cases that will have to be handled. And
lots of objects that cannot be represented as documents.
5. Serialize Document. One thing the parser does not preserve is
whether a Value was flow-style or not, so it will be impossible to
do round-trip formatting preservation. That's currently a non-goal,
and I haven't decided yet if flow-style output should be based on
some heuristic (number/length of values in container) or just never
emitted. Lack of round-trip preservation does make using this as a
general purpose config format a lot more dubious, so I will have to
think about this some more.
6. Document to JSON. Why not? I will hand roll this and it will suck.
And then everything will be perfect and never need to be touched again.
2023-09-18 00:01:36 -07:00
|
|
|
},
|
2023-09-17 19:47:18 -07:00
|
|
|
else => continue :charloop,
|
|
|
|
},
|
|
|
|
.want_list_separator => switch (char) {
|
|
|
|
' ', '\t' => continue :charloop,
|
|
|
|
',' => {
|
2023-09-23 17:27:21 -07:00
|
|
|
item_start = idx;
|
2023-09-23 13:29:49 -07:00
|
|
|
state = .want_list_item;
|
2023-09-17 19:47:18 -07:00
|
|
|
},
|
2023-09-23 13:29:49 -07:00
|
|
|
']' => state = try popStack(&stack),
|
2023-09-17 19:47:18 -07:00
|
|
|
else => return error.BadToken,
|
|
|
|
},
|
|
|
|
.want_map_key => switch (char) {
|
|
|
|
' ', '\t' => continue :charloop,
|
|
|
|
// forbid these characters so that flow dictionary keys cannot start
|
|
|
|
// with characters that regular dictionary keys cannot start with
|
|
|
|
// (even though they're unambiguous in this specific context).
|
2023-09-23 01:07:04 -07:00
|
|
|
'{', '[', '#', '-', '>', '|', ',' => return error.BadToken,
|
2023-09-17 19:47:18 -07:00
|
|
|
':' => {
|
|
|
|
// we have an empty map key
|
2023-09-17 23:09:26 -07:00
|
|
|
dangling_key = "";
|
2023-09-23 13:29:49 -07:00
|
|
|
state = .want_map_value;
|
2023-09-17 19:47:18 -07:00
|
|
|
},
|
2023-09-23 13:29:49 -07:00
|
|
|
'}' => state = try popStack(&stack),
|
2023-09-17 19:47:18 -07:00
|
|
|
else => {
|
2023-09-23 17:27:21 -07:00
|
|
|
item_start = idx;
|
2023-09-23 13:29:49 -07:00
|
|
|
state = .consuming_map_key;
|
2023-09-17 19:47:18 -07:00
|
|
|
},
|
|
|
|
},
|
|
|
|
.consuming_map_key => switch (char) {
|
|
|
|
':' => {
|
2023-09-23 17:27:21 -07:00
|
|
|
dangling_key = try alloc.dupe(u8, contents[item_start..idx]);
|
2023-09-23 13:29:49 -07:00
|
|
|
state = .want_map_value;
|
2023-09-17 19:47:18 -07:00
|
|
|
},
|
|
|
|
else => continue :charloop,
|
|
|
|
},
|
|
|
|
.want_map_value => switch (char) {
|
|
|
|
' ', '\t' => continue :charloop,
|
|
|
|
',' => {
|
2023-09-23 13:29:49 -07:00
|
|
|
const tip = try getStackTip(stack);
|
2023-09-17 19:47:18 -07:00
|
|
|
try Parser.putMap(
|
2023-09-23 17:27:21 -07:00
|
|
|
&tip.flow_map,
|
2023-09-17 23:09:26 -07:00
|
|
|
dangling_key.?,
|
2023-09-23 13:29:49 -07:00
|
|
|
Value.newScalar(alloc),
|
2023-09-17 19:47:18 -07:00
|
|
|
dupe_behavior,
|
|
|
|
);
|
|
|
|
|
2023-09-17 23:09:26 -07:00
|
|
|
dangling_key = null;
|
2023-09-23 13:29:49 -07:00
|
|
|
state = .want_map_key;
|
2023-09-17 19:47:18 -07:00
|
|
|
},
|
|
|
|
'[' => {
|
2023-09-23 13:29:49 -07:00
|
|
|
const tip = try getStackTip(stack);
|
2023-09-17 19:47:18 -07:00
|
|
|
|
|
|
|
const new_list = try Parser.putMapGetValue(
|
2023-09-23 17:27:21 -07:00
|
|
|
&tip.flow_map,
|
2023-09-17 23:09:26 -07:00
|
|
|
dangling_key.?,
|
2023-09-23 13:29:49 -07:00
|
|
|
Value.newFlowList(alloc),
|
2023-09-17 19:47:18 -07:00
|
|
|
dupe_behavior,
|
|
|
|
);
|
|
|
|
|
2023-09-23 17:27:21 -07:00
|
|
|
try stack.append(new_list);
|
2023-09-17 23:09:26 -07:00
|
|
|
dangling_key = null;
|
2023-09-23 17:27:21 -07:00
|
|
|
item_start = idx + 1;
|
2023-09-23 13:29:49 -07:00
|
|
|
state = .want_list_item;
|
2023-09-17 19:47:18 -07:00
|
|
|
},
|
|
|
|
'{' => {
|
2023-09-23 13:29:49 -07:00
|
|
|
const tip = try getStackTip(stack);
|
2023-09-17 19:47:18 -07:00
|
|
|
|
|
|
|
const new_map = try Parser.putMapGetValue(
|
2023-09-23 17:27:21 -07:00
|
|
|
&tip.flow_map,
|
2023-09-17 23:09:26 -07:00
|
|
|
dangling_key.?,
|
2023-09-23 13:29:49 -07:00
|
|
|
Value.newFlowMap(alloc),
|
2023-09-17 19:47:18 -07:00
|
|
|
dupe_behavior,
|
|
|
|
);
|
|
|
|
|
2023-09-23 17:27:21 -07:00
|
|
|
try stack.append(new_map);
|
2023-09-17 23:09:26 -07:00
|
|
|
dangling_key = null;
|
2023-09-23 13:29:49 -07:00
|
|
|
state = .want_map_key;
|
2023-09-17 19:47:18 -07:00
|
|
|
},
|
|
|
|
'}' => {
|
|
|
|
// the value is an empty string and this map is closed
|
2023-09-23 13:29:49 -07:00
|
|
|
const tip = try getStackTip(stack);
|
2023-09-17 19:47:18 -07:00
|
|
|
try Parser.putMap(
|
2023-09-23 17:27:21 -07:00
|
|
|
&tip.flow_map,
|
2023-09-17 23:09:26 -07:00
|
|
|
dangling_key.?,
|
2023-09-23 13:29:49 -07:00
|
|
|
Value.newScalar(alloc),
|
2023-09-17 19:47:18 -07:00
|
|
|
dupe_behavior,
|
|
|
|
);
|
|
|
|
|
2023-09-17 23:09:26 -07:00
|
|
|
dangling_key = null;
|
2023-09-23 13:29:49 -07:00
|
|
|
state = try popStack(&stack);
|
2023-09-17 19:47:18 -07:00
|
|
|
},
|
|
|
|
else => {
|
2023-09-23 17:27:21 -07:00
|
|
|
item_start = idx;
|
2023-09-23 13:29:49 -07:00
|
|
|
state = .consuming_map_value;
|
2023-09-17 19:47:18 -07:00
|
|
|
},
|
|
|
|
},
|
|
|
|
.consuming_map_value => switch (char) {
|
|
|
|
',', '}' => |term| {
|
2023-09-23 13:29:49 -07:00
|
|
|
const tip = try getStackTip(stack);
|
2023-09-17 19:47:18 -07:00
|
|
|
try Parser.putMap(
|
2023-09-23 17:27:21 -07:00
|
|
|
&tip.flow_map,
|
2023-09-17 23:09:26 -07:00
|
|
|
dangling_key.?,
|
2023-09-23 17:27:21 -07:00
|
|
|
try Value.fromScalar(alloc, contents[item_start..idx]),
|
2023-09-17 19:47:18 -07:00
|
|
|
dupe_behavior,
|
|
|
|
);
|
2023-09-17 23:09:26 -07:00
|
|
|
dangling_key = null;
|
2023-09-23 13:29:49 -07:00
|
|
|
state = .want_map_key;
|
|
|
|
if (term == '}') state = try popStack(&stack);
|
2023-09-17 19:47:18 -07:00
|
|
|
},
|
|
|
|
else => continue :charloop,
|
|
|
|
},
|
|
|
|
.want_map_separator => switch (char) {
|
|
|
|
' ', '\t' => continue :charloop,
|
2023-09-23 13:29:49 -07:00
|
|
|
',' => state = .want_map_key,
|
|
|
|
'}' => state = try popStack(&stack),
|
2023-09-17 19:47:18 -07:00
|
|
|
else => return error.BadToken,
|
|
|
|
},
|
|
|
|
// the root value was closed but there are characters remaining
|
|
|
|
// in the buffer
|
|
|
|
.done => return error.BadState,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// we ran out of characters while still in the middle of an object
|
2023-09-23 13:29:49 -07:00
|
|
|
if (state != .done) return error.BadState;
|
|
|
|
|
|
|
|
return root;
|
|
|
|
}
|
|
|
|
|
|
|
|
inline fn appendListGetValue(list: *Value.List, value: Value) Error!*Value {
|
|
|
|
try list.append(value);
|
|
|
|
return &list.items[list.items.len - 1];
|
|
|
|
}
|
|
|
|
|
|
|
|
inline fn putMap(map: *Value.Map, key: []const u8, value: Value, dupe_behavior: DuplicateKeyBehavior) Error!void {
|
|
|
|
_ = try putMapGetValue(map, key, value, dupe_behavior);
|
|
|
|
}
|
|
|
|
|
|
|
|
inline fn putMapGetValue(map: *Value.Map, key: []const u8, value: Value, dupe_behavior: DuplicateKeyBehavior) Error!*Value {
|
|
|
|
const gop = try map.getOrPut(key);
|
|
|
|
|
|
|
|
if (gop.found_existing)
|
|
|
|
switch (dupe_behavior) {
|
|
|
|
.fail => return error.DuplicateKey,
|
|
|
|
.use_first => {},
|
|
|
|
.use_last => gop.value_ptr.* = value,
|
|
|
|
}
|
|
|
|
else
|
|
|
|
gop.value_ptr.* = value;
|
|
|
|
|
|
|
|
return gop.value_ptr;
|
|
|
|
}
|
2023-09-17 19:47:18 -07:00
|
|
|
};
|