Compare commits

...

7 Commits

Author SHA1 Message Date
Jarred Sumner
f1c6df54bb Update yaml_lexer.zig 2025-01-06 02:13:49 -08:00
Jarred Sumner
43abc3837b Update yaml_lexer.zig 2025-01-06 01:44:59 -08:00
Jarred Sumner
d2bbf36c04 a bit further 2025-01-06 01:23:20 -08:00
Jarred Sumner
e5efada28b lol it compiled 2025-01-05 22:35:31 -08:00
Jarred Sumner
9e01a916e4 more 2025-01-05 22:13:54 -08:00
Jarred Sumner
d48f345309 initial commit 2025-01-05 22:01:46 -08:00
Jarred Sumner
6b578ad033 Delete some code 2025-01-05 22:01:39 -08:00
14 changed files with 2304 additions and 91 deletions

View File

@@ -343,6 +343,7 @@ pub const Api = struct {
text,
sqlite,
html,
yaml,
_,
pub fn jsonStringify(self: @This(), writer: anytype) !void {

View File

@@ -58,6 +58,7 @@ pub const BunObject = struct {
pub const SHA512 = toJSGetter(Crypto.SHA512.getter);
pub const SHA512_256 = toJSGetter(Crypto.SHA512_256.getter);
pub const TOML = toJSGetter(Bun.getTOMLObject);
pub const YAML = toJSGetter(Bun.getYAMLObject);
pub const Transpiler = toJSGetter(Bun.getTranspilerConstructor);
pub const argv = toJSGetter(Bun.getArgv);
pub const cwd = toJSGetter(Bun.getCWD);
@@ -117,6 +118,7 @@ pub const BunObject = struct {
@export(BunObject.SHA512_256, .{ .name = getterName("SHA512_256") });
@export(BunObject.TOML, .{ .name = getterName("TOML") });
@export(BunObject.YAML, .{ .name = getterName("YAML") });
@export(BunObject.Glob, .{ .name = getterName("Glob") });
@export(BunObject.Transpiler, .{ .name = getterName("Transpiler") });
@export(BunObject.argv, .{ .name = getterName("argv") });
@@ -3394,6 +3396,10 @@ pub fn getTOMLObject(globalThis: *JSC.JSGlobalObject, _: *JSC.JSObject) JSC.JSVa
return TOMLObject.create(globalThis);
}
pub fn getYAMLObject(globalThis: *JSC.JSGlobalObject, _: *JSC.JSObject) JSC.JSValue {
return YAMLObject.create(globalThis);
}
pub fn getGlobConstructor(globalThis: *JSC.JSGlobalObject, _: *JSC.JSObject) JSC.JSValue {
return JSC.API.Glob.getConstructor(globalThis);
}
@@ -3500,61 +3506,73 @@ const UnsafeObject = struct {
}
};
const TOMLObject = struct {
const TOMLParser = @import("../../toml/toml_parser.zig").TOML;
fn TOMLLikeParser(comptime Parser: anytype, comptime default_name: []const u8) type {
return struct {
const ThisParser = Parser;
const name = default_name;
pub fn create(globalThis: *JSC.JSGlobalObject) JSC.JSValue {
const object = JSValue.createEmptyObject(globalThis, 1);
object.put(
globalThis,
ZigString.static("parse"),
JSC.createCallback(
pub fn create(globalThis: *JSC.JSGlobalObject) JSC.JSValue {
const object = JSValue.createEmptyObject(globalThis, 1);
object.put(
globalThis,
ZigString.static("parse"),
1,
parse,
),
);
JSC.createCallback(
globalThis,
ZigString.static("parse"),
1,
parse,
),
);
return object;
}
pub fn parse(
globalThis: *JSC.JSGlobalObject,
callframe: *JSC.CallFrame,
) bun.JSError!JSC.JSValue {
var arena = bun.ArenaAllocator.init(globalThis.allocator());
const allocator = arena.allocator();
defer arena.deinit();
var log = logger.Log.init(default_allocator);
const arguments = callframe.arguments_old(1).slice();
if (arguments.len == 0 or arguments[0].isEmptyOrUndefinedOrNull()) {
return globalThis.throwInvalidArguments("Expected a string to parse", .{});
return object;
}
var input_slice = arguments[0].toSlice(globalThis, bun.default_allocator);
defer input_slice.deinit();
var source = logger.Source.initPathString("input.toml", input_slice.slice());
const parse_result = TOMLParser.parse(&source, &log, allocator, false) catch {
return globalThis.throwValue(log.toJS(globalThis, default_allocator, "Failed to parse toml"));
};
pub fn parse(
globalThis: *JSC.JSGlobalObject,
callframe: *JSC.CallFrame,
) bun.JSError!JSC.JSValue {
var arena = bun.ArenaAllocator.init(globalThis.allocator());
const allocator = arena.allocator();
defer arena.deinit();
var log = logger.Log.init(default_allocator);
defer log.deinit();
const arguments = callframe.arguments_old(1).slice();
if (arguments.len == 0 or arguments[0].isEmptyOrUndefinedOrNull()) {
return globalThis.throwInvalidArguments("Expected a string to parse", .{});
}
// for now...
const buffer_writer = js_printer.BufferWriter.init(allocator) catch {
return globalThis.throwValue(log.toJS(globalThis, default_allocator, "Failed to print toml"));
};
var writer = js_printer.BufferPrinter.init(buffer_writer);
_ = js_printer.printJSON(*js_printer.BufferPrinter, &writer, parse_result, &source, .{}) catch {
return globalThis.throwValue(log.toJS(globalThis, default_allocator, "Failed to print toml"));
};
var input_slice: JSC.Node.StringOrBuffer = JSC.Node.StringOrBuffer.fromJS(globalThis, bun.default_allocator, arguments[0]) orelse {
if (!globalThis.hasException()) {
return globalThis.throwInvalidArguments("Expected a string to parse", .{});
}
return error.JSError;
};
defer input_slice.deinit();
var source = logger.Source.initPathString(name, input_slice.slice());
const parse_result = ThisParser.parse(&source, &log, allocator, false) catch {
return globalThis.throwValue(log.toJS(globalThis, default_allocator, "Failed to parse toml"));
};
const slice = writer.ctx.buffer.slice();
var out = bun.String.fromUTF8(slice);
defer out.deref();
// for now...
const buffer_writer = js_printer.BufferWriter.init(allocator) catch {
return globalThis.throwValue(log.toJS(globalThis, default_allocator, "Failed to print toml"));
};
var writer = js_printer.BufferPrinter.init(buffer_writer);
_ = js_printer.printJSON(*js_printer.BufferPrinter, &writer, parse_result, &source, .{}) catch {
return globalThis.throwValue(log.toJS(globalThis, default_allocator, "Failed to print toml"));
};
return out.toJSByParseJSON(globalThis);
}
};
const slice = writer.ctx.buffer.slice();
var out = bun.String.fromUTF8(slice);
defer out.deref();
return out.toJSByParseJSON(globalThis);
}
};
}
pub const TOMLObject = TOMLLikeParser(@import("../../toml/toml_parser.zig").TOML, "input.toml");
pub const YAMLObject = TOMLLikeParser(@import("../../yaml/yaml_parser.zig").YAML, "input.yaml");
const Debugger = JSC.Debugger;

View File

@@ -17,20 +17,21 @@
macro(SHA512_256) \
macro(TOML) \
macro(Transpiler) \
macro(YAML) \
macro(argv) \
macro(assetPrefix) \
macro(cwd) \
macro(embeddedFiles) \
macro(enableANSIColors) \
macro(hash) \
macro(inspect) \
macro(main) \
macro(origin) \
macro(semver) \
macro(stderr) \
macro(stdin) \
macro(stdout) \
macro(unsafe) \
macro(semver) \
macro(embeddedFiles) \
// --- Callbacks ---
#define FOR_EACH_CALLBACK(macro) \

View File

@@ -628,6 +628,7 @@ JSC_DEFINE_HOST_FUNCTION(functionFileURLToPath, (JSC::JSGlobalObject * globalObj
SHA512 BunObject_getter_wrap_SHA512 DontDelete|PropertyCallback
SHA512_256 BunObject_getter_wrap_SHA512_256 DontDelete|PropertyCallback
TOML BunObject_getter_wrap_TOML DontDelete|PropertyCallback
YAML BunObject_getter_wrap_YAML DontDelete|PropertyCallback
Transpiler BunObject_getter_wrap_Transpiler DontDelete|PropertyCallback
embeddedFiles BunObject_getter_wrap_embeddedFiles DontDelete|PropertyCallback
allocUnsafe BunObject_callback_allocUnsafe DontDelete|Function 1

View File

@@ -245,13 +245,15 @@ OnLoadResult handleOnLoadResultNotPromise(Zig::GlobalObject* globalObject, JSC::
loader = BunLoaderTypeJSON;
} else if (loaderString == "toml"_s) {
loader = BunLoaderTypeTOML;
} else if (loaderString == "yaml"_s) {
loader = BunLoaderTypeYAML;
}
}
}
}
if (UNLIKELY(loader == BunLoaderTypeNone)) {
throwException(globalObject, scope, createError(globalObject, "Expected loader to be one of \"js\", \"jsx\", \"object\", \"ts\", \"tsx\", \"toml\", or \"json\""_s));
throwException(globalObject, scope, createError(globalObject, "Expected loader to be one of \"js\", \"jsx\", \"object\", \"ts\", \"tsx\", \"toml\", \"yaml\", or \"json\""_s));
result.value.error = scope.exception();
return result;
}

View File

@@ -220,6 +220,7 @@ const BunLoaderType BunLoaderTypeJSON = 6;
const BunLoaderType BunLoaderTypeTOML = 7;
const BunLoaderType BunLoaderTypeWASM = 8;
const BunLoaderType BunLoaderTypeNAPI = 9;
const BunLoaderType BunLoaderTypeYAML = 10;
#pragma mark - Stream

View File

@@ -1519,7 +1519,7 @@ pub const ModuleLoader = struct {
}
switch (loader) {
.js, .jsx, .ts, .tsx, .json, .toml, .text => {
.js, .jsx, .ts, .tsx, .json, .toml, .yaml, .text => {
jsc_vm.transpiled_count += 1;
jsc_vm.transpiler.resetStore();
const hash = JSC.GenericWatcher.getHash(path.text);
@@ -1796,7 +1796,7 @@ pub const ModuleLoader = struct {
.already_bundled = true,
.hash = 0,
.bytecode_cache = if (bytecode_slice.len > 0) bytecode_slice.ptr else null,
.bytecode_cache_size = if (bytecode_slice.len > 0) bytecode_slice.len else 0,
.bytecode_cache_size = bytecode_slice.len,
.is_commonjs_module = parse_result.already_bundled.isCommonJS(),
};
}
@@ -2327,6 +2327,8 @@ pub const ModuleLoader = struct {
loader = .ts;
} else if (attribute.eqlComptime("tsx")) {
loader = .tsx;
} else if (attribute.eqlComptime("yaml")) {
loader = .yaml;
}
}

View File

@@ -101,6 +101,7 @@ const URL = @import("../url.zig").URL;
const Linker = linker.Linker;
const Resolver = _resolver.Resolver;
const TOML = @import("../toml/toml_parser.zig").TOML;
const YAML = @import("../yaml/yaml_parser.zig").YAML;
const EntryPoints = @import("./entry_points.zig");
const Dependency = js_ast.Dependency;
const JSAst = js_ast.BundledAst;
@@ -3667,6 +3668,12 @@ pub const ParseTask = struct {
const root = try TOML.parse(&source, log, allocator, false);
return JSAst.init((try js_parser.newLazyExportAST(allocator, transpiler.options.define, opts, log, root, &source, "")).?);
},
.yaml => {
const trace = tracer(@src(), "ParseYAML");
defer trace.end();
const root = try YAML.parse(&source, log, allocator, false);
return JSAst.init((try js_parser.newLazyExportAST(allocator, transpiler.options.define, opts, log, root, &source, "")).?);
},
.text => {
const root = Expr.init(E.String, E.String{
.data = source.contents,

View File

@@ -1917,6 +1917,10 @@ pub const E = struct {
return if (asProperty(self, key)) |query| query.expr else @as(?Expr, null);
}
pub fn getPtr(self: *const Object, key: string) ?*Expr {
return if (asProperty(self, key)) |query| &self.properties.slice()[query.i].value.? else null;
}
pub fn toJS(this: *Object, allocator: std.mem.Allocator, globalObject: *JSC.JSGlobalObject) ToJSError!JSC.JSValue {
var obj = JSC.JSValue.createEmptyObject(globalObject, this.properties.len);
obj.protect();
@@ -1965,7 +1969,7 @@ pub const E = struct {
// this is terribly, shamefully slow
pub fn setRope(self: *Object, rope: *const Rope, allocator: std.mem.Allocator, value: Expr) SetError!void {
if (self.get(rope.head.data.e_string.data)) |existing| {
if (self.getPtr(rope.head.data.e_string.data)) |existing| {
switch (existing.data) {
.e_array => |array| {
if (rope.next == null) {
@@ -1993,6 +1997,18 @@ pub const E = struct {
return error.Clobber;
},
.e_null => {
// null can replace.
if (rope.next) |next| {
var obj = Expr.init(E.Object, E.Object{ .properties = .{} }, rope.head.loc);
existing.* = obj;
try obj.data.e_object.setRope(next, allocator, value);
return;
}
existing.* = value;
return;
},
else => {
return error.Clobber;
},
@@ -3398,6 +3414,10 @@ pub const Expr = struct {
return if (asProperty(expr, name)) |query| query.expr else null;
}
pub fn getPtr(expr: *const Expr, name: string) ?*Expr {
return if (asProperty(expr, name)) |*query| &expr.data.e_object.properties.slice()[query.i] else null;
}
/// Don't use this if you care about performance.
///
/// Sets the value of a property, creating it if it doesn't exist.

View File

@@ -645,6 +645,7 @@ pub const Loader = enum(u8) {
sqlite,
sqlite_embedded,
html,
yaml,
pub fn disableHTML(this: Loader) Loader {
return switch (this) {
@@ -684,7 +685,8 @@ pub const Loader = enum(u8) {
return switch (this) {
.jsx, .js, .ts, .tsx => bun.http.MimeType.javascript,
.css => bun.http.MimeType.css,
.toml, .json => bun.http.MimeType.json,
.yaml, .toml, .json => bun.http.MimeType.json,
.wasm => bun.http.MimeType.wasm,
.html => bun.http.MimeType.html,
else => bun.http.MimeType.other,
@@ -723,6 +725,7 @@ pub const Loader = enum(u8) {
map.set(.text, "input.txt");
map.set(.bunsh, "input.sh");
map.set(.html, "input.html");
map.set(.yaml, "input.yaml");
break :brk map;
};
@@ -769,6 +772,7 @@ pub const Loader = enum(u8) {
.{ "sqlite", .sqlite },
.{ "sqlite_embedded", .sqlite_embedded },
.{ "html", .html },
.{ "yaml", .yaml },
});
pub const api_names = bun.ComptimeStringMap(Api.Loader, .{
@@ -793,6 +797,7 @@ pub const Loader = enum(u8) {
.{ "sh", .file },
.{ "sqlite", .sqlite },
.{ "html", .html },
.{ "yaml", .yaml },
});
pub fn fromString(slice_: string) ?Loader {
@@ -822,6 +827,7 @@ pub const Loader = enum(u8) {
.file, .bunsh => .file,
.json => .json,
.toml => .toml,
.yaml => .yaml,
.wasm => .wasm,
.napi => .napi,
.base64 => .base64,
@@ -842,6 +848,7 @@ pub const Loader = enum(u8) {
.file => .file,
.json => .json,
.toml => .toml,
.yaml => .yaml,
.wasm => .wasm,
.napi => .napi,
.base64 => .base64,
@@ -872,8 +879,8 @@ pub const Loader = enum(u8) {
return switch (loader) {
.jsx, .js, .ts, .tsx, .json => true,
// toml is included because we can serialize to the same AST as JSON
.toml => true,
// toml and yaml are included because we can serialize to the same AST as JSON
.toml, .yaml => true,
else => false,
};
@@ -903,6 +910,8 @@ const default_loaders_posix = .{
.{ ".cts", .ts },
.{ ".toml", .toml },
.{ ".yaml", .yaml },
.{ ".yml", .yaml },
.{ ".wasm", .wasm },
.{ ".node", .napi },
.{ ".txt", .text },
@@ -1294,7 +1303,8 @@ const default_loader_ext = [_]string{
".ts", ".tsx",
".mts", ".cts",
".toml", ".wasm",
".toml", ".yaml",
".yml", ".wasm",
".txt", ".text",
};
@@ -1320,6 +1330,8 @@ const node_modules_default_loader_ext = [_]string{
".wasm",
".text",
".html",
".yaml",
".yml",
};
pub const ResolveFileExtensions = struct {

View File

@@ -46,36 +46,6 @@ const locModuleScope = logger.Loc.Empty;
const LEXER_DEBUGGER_WORKAROUND = false;
const IdentityContext = @import("../identity_context.zig").IdentityContext;
const HashMapPool = struct {
const HashMap = std.HashMap(u64, void, IdentityContext, 80);
const LinkedList = std.SinglyLinkedList(HashMap);
threadlocal var list: LinkedList = undefined;
threadlocal var loaded: bool = false;
pub fn get(_: std.mem.Allocator) *LinkedList.Node {
if (loaded) {
if (list.popFirst()) |node| {
node.data.clearRetainingCapacity();
return node;
}
}
const new_node = default_allocator.create(LinkedList.Node) catch unreachable;
new_node.* = LinkedList.Node{ .data = HashMap.initContext(default_allocator, IdentityContext{}) };
return new_node;
}
pub fn release(node: *LinkedList.Node) void {
if (loaded) {
list.prepend(node);
return;
}
list = LinkedList{ .first = node };
loaded = true;
}
};
pub const TOML = struct {
lexer: Lexer,
log: *logger.Log,

View File

@@ -52,7 +52,7 @@ const TOML = @import("./toml/toml_parser.zig").TOML;
const JSC = bun.JSC;
const PackageManager = @import("./install/install.zig").PackageManager;
const DataURL = @import("./resolver/data_url.zig").DataURL;
const YAML = @import("./yaml/yaml_parser.zig").YAML;
pub fn MacroJSValueType_() type {
if (comptime JSC.is_bindgen) {
return struct {
@@ -866,7 +866,7 @@ pub const Transpiler = struct {
};
switch (loader) {
.jsx, .tsx, .js, .ts, .json, .toml, .text => {
.jsx, .tsx, .js, .ts, .json, .toml, .yaml, .text => {
var result = transpiler.parse(
ParseOptions{
.allocator = transpiler.allocator,
@@ -1452,8 +1452,7 @@ pub const Transpiler = struct {
},
};
},
// TODO: use lazy export AST
inline .toml, .json => |kind| {
inline .toml, .json, .yaml => |kind| {
var expr = if (kind == .json)
// We allow importing tsconfig.*.json or jsconfig.*.json with comments
// These files implicitly become JSONC files, which aligns with the behavior of text editors.
@@ -1463,6 +1462,8 @@ pub const Transpiler = struct {
JSON.parse(&source, transpiler.log, allocator, false) catch return null
else if (kind == .toml)
TOML.parse(&source, transpiler.log, allocator, false) catch return null
else if (kind == .yaml)
YAML.parse(&source, transpiler.log, allocator, false) catch return null
else
@compileError("unreachable");
@@ -1717,7 +1718,7 @@ pub const Transpiler = struct {
),
};
},
.toml, .json => {
.toml, .json, .yaml => {
return ServeResult{
.file = options.OutputFile.initPending(loader, resolved),
.mime_type = MimeType.transpiled_json,

1622
src/yaml/yaml_lexer.zig Normal file

File diff suppressed because it is too large Load Diff

555
src/yaml/yaml_parser.zig Normal file
View File

@@ -0,0 +1,555 @@
const std = @import("std");
const logger = bun.logger;
const js_lexer = bun.js_lexer;
const js_ast = bun.JSAst;
const bun = @import("root").bun;
const string = bun.string;
const Output = bun.Output;
const Global = bun.Global;
const Environment = bun.Environment;
const strings = bun.strings;
const MutableString = bun.MutableString;
const stringZ = bun.stringZ;
const default_allocator = bun.default_allocator;
const C = bun.C;
const expect = std.testing.expect;
const BindingNodeIndex = js_ast.BindingNodeIndex;
const StmtNodeIndex = js_ast.StmtNodeIndex;
const ExprNodeIndex = js_ast.ExprNodeIndex;
const ExprNodeList = js_ast.ExprNodeList;
const StmtNodeList = js_ast.StmtNodeList;
const BindingNodeList = js_ast.BindingNodeList;
const assert = bun.assert;
const LocRef = js_ast.LocRef;
const S = js_ast.S;
const B = js_ast.B;
const G = js_ast.G;
const T = js_lexer.T;
const E = js_ast.E;
const Stmt = js_ast.Stmt;
pub const Expr = js_ast.Expr;
const Binding = js_ast.Binding;
const Symbol = js_ast.Symbol;
const Level = js_ast.Op.Level;
const Op = js_ast.Op;
const Scope = js_ast.Scope;
const locModuleScope = logger.Loc.Empty;
const Lexer = @import("./yaml_lexer.zig").Lexer;
pub const YAML = struct {
lexer: Lexer,
log: *logger.Log,
allocator: std.mem.Allocator,
pub fn init(allocator: std.mem.Allocator, source_: logger.Source, log: *logger.Log, redact_logs: bool) !YAML {
return YAML{
.lexer = try Lexer.init(log, source_, allocator, redact_logs),
.allocator = allocator,
.log = log,
};
}
pub inline fn source(p: *const YAML) *const logger.Source {
return &p.lexer.source;
}
pub fn e(_: *YAML, t: anytype, loc: logger.Loc) Expr {
const Type = @TypeOf(t);
if (@typeInfo(Type) == .Pointer) {
return Expr.init(std.meta.Child(Type), t.*, loc);
} else {
return Expr.init(Type, t, loc);
}
}
const Rope = js_ast.E.Object.Rope;
pub fn parseKeySegment(p: *YAML) anyerror!?Expr {
const loc = p.lexer.loc();
switch (p.lexer.token) {
.t_string_literal => {
const str = p.lexer.toString(loc);
try p.lexer.next();
return str;
},
.t_identifier => {
const str = E.String{ .data = p.lexer.identifier };
try p.lexer.next();
return p.e(str, loc);
},
.t_false => {
try p.lexer.next();
return p.e(
E.String{
.data = "false",
},
loc,
);
},
.t_true => {
try p.lexer.next();
return p.e(
E.String{
.data = "true",
},
loc,
);
},
.t_numeric_literal => {
const literal = p.lexer.raw();
try p.lexer.next();
return p.e(E.String{ .data = literal }, loc);
},
else => return null,
}
}
pub fn parseKey(p: *YAML, allocator: std.mem.Allocator) anyerror!*Rope {
var rope = try allocator.create(Rope);
const head = rope;
rope.* = .{
.head = (try p.parseKeySegment()) orelse {
try p.lexer.expectedString("key");
return error.SyntaxError;
},
.next = null,
};
while (p.lexer.token == .t_dot) {
try p.lexer.next();
rope = try rope.append((try p.parseKeySegment()) orelse break, allocator);
}
return head;
}
pub fn parse(source_: *const logger.Source, log: *logger.Log, allocator: std.mem.Allocator, redact_logs: bool) !Expr {
switch (source_.contents.len) {
// This is to be consistent with how disabled JS files are handled
0 => {
return Expr{ .loc = logger.Loc{ .start = 0 }, .data = Expr.init(E.Object, E.Object{}, logger.Loc.Empty).data };
},
else => {},
}
var parser = try YAML.init(allocator, source_.*, log, redact_logs);
return try parser.runParser();
}
fn runParser(p: *YAML) anyerror!Expr {
var root_expr: ?Expr = null;
var stack = std.heap.stackFallback(@sizeOf(Rope) * 6, p.allocator);
const key_allocator = stack.get();
while (true) {
switch (p.lexer.token) {
.t_end_of_file => {
return root_expr orelse p.e(E.Object{}, p.lexer.loc());
},
.t_document_start => {
try p.lexer.next();
continue;
},
.t_document_end => {
try p.lexer.next();
continue;
},
.t_dash => {
// Start of sequence item
if (root_expr == null) {
// First dash, create the sequence
root_expr = p.e(E.Array{
.items = .{},
.is_single_line = false,
}, p.lexer.loc());
}
if (root_expr != null and root_expr.?.data != .e_array) {
try p.lexer.addDefaultError("Top-level sequence must be an array or object");
return error.SyntaxError;
}
const array = root_expr.?.data.e_array;
const value = try p.parseValue();
try array.push(p.allocator, value);
continue;
},
.t_newline => {
try p.lexer.next();
continue;
},
.t_identifier, .t_string_literal => {
const initial_indent = p.lexer.current_indent;
// Create root object if needed
if (root_expr == null) {
root_expr = p.e(E.Object{}, p.lexer.loc());
}
if (root_expr.?.data != .e_object) {
try p.lexer.addDefaultError("Top-level sequence must be an array or object");
return error.SyntaxError;
}
const head = root_expr.?.data.e_object;
// Key-value pair
const key = try key_allocator.create(Rope);
key.* = .{
.head = p.lexer.toPropertyKey(p.lexer.loc()),
.next = null,
};
try p.lexer.next();
try p.lexer.expect(.t_colon);
while (p.lexer.token == .t_newline) {
try p.lexer.next();
}
const value = value: {
const new_indent = p.lexer.current_indent;
if (new_indent > initial_indent) {
const value = try p.parseObjectOrArraySequence(p.lexer.loc(), new_indent);
break :value value;
} else if (p.lexer.token == .t_dash) {
try p.lexer.addDefaultError("An array cannot be nested inside an object");
return error.SyntaxError;
} else if (p.lexer.token == .t_end_of_file) {
break :value p.e(E.Null{}, p.lexer.loc());
}
break :value try p.parseValue();
};
head.setRope(key, p.allocator, value) catch |err| {
switch (err) {
error.Clobber => {
try p.lexer.addDefaultError("Cannot redefine key");
return error.SyntaxError;
},
else => return err,
}
};
// Handle any trailing newlines after the value
while (p.lexer.token == .t_newline) {
try p.lexer.next();
}
},
else => {
try p.lexer.unexpected();
return error.SyntaxError;
},
}
}
return root_expr orelse p.e(E.Object{}, p.lexer.loc());
}
fn parseObjectOrArraySequence(p: *YAML, loc: logger.Loc, indent: u16) anyerror!Expr {
// Check what follows to determine if it's an array or object
if (p.lexer.token == .t_dash) {
// The start of an array sequence
const array = p.e(E.Array{
.items = .{},
}, loc);
while (p.lexer.token == .t_dash) {
try p.lexer.next();
while (p.lexer.token == .t_newline) {
try p.lexer.next();
}
if (p.lexer.token == .t_end_of_file or p.lexer.current_indent < indent) {
break;
}
if (p.lexer.current_indent > indent) {
try array.data.e_array.push(p.allocator, try p.runParser());
} else {
try array.data.e_array.push(p.allocator, try p.parseValue());
}
while (p.lexer.token == .t_newline) {
try p.lexer.next();
}
}
return array;
} else {
var root: ?Expr = null;
// Parse key-value pairs at this indentation level
while (true) {
while (p.lexer.token == .t_newline) {
try p.lexer.next();
}
if (p.lexer.token == .t_end_of_file or p.lexer.current_indent < indent) {
break;
}
const key = try p.parseKey(p.allocator);
try p.lexer.expect(.t_colon);
while (p.lexer.token == .t_newline) {
try p.lexer.next();
}
// A single object with { [key]: null }
if (p.lexer.token == .t_end_of_file or p.lexer.current_indent < indent) {
if (root == null) {
root = p.e(E.Object{}, loc);
}
root.?.data.e_object.setRope(key, p.allocator, p.e(E.Null{}, loc)) catch {
try p.lexer.addDefaultError("Cannot redefine key");
return error.SyntaxError;
};
break;
}
// Handle potential indent after the colon
const value = if (p.lexer.current_indent > indent)
try p.runParser()
else
try p.parseValue();
if (root == null) {
root = p.e(E.Object{}, loc);
}
root.?.data.e_object.setRope(key, p.allocator, value) catch |err| {
switch (err) {
error.Clobber => {
try p.lexer.addDefaultError("Cannot redefine key");
return error.SyntaxError;
},
else => return err,
}
};
}
return root orelse p.e(E.Null{}, loc);
}
}
pub fn parseValue(p: *YAML) anyerror!Expr {
const loc = p.lexer.loc();
// Handle tags - type annotations like !!str, !!int, etc.
// Example: !!int "123" -> converts to number 123
if (p.lexer.token == .t_tag) {
try p.lexer.next();
p.lexer.current_tag = p.lexer.raw();
try p.lexer.next();
}
// Handle anchors - define reusable nodes
// Example: &anchor_name value
if (p.lexer.token == .t_anchor) {
try p.lexer.next();
p.lexer.current_anchor = p.lexer.raw();
try p.lexer.next();
}
var value = switch (p.lexer.token) {
// Handle aliases - reference previously anchored nodes
// Example: *anchor_name
.t_alias => brk: {
try p.lexer.next();
const alias_name = p.lexer.raw();
try p.lexer.next();
break :brk p.lexer.anchors.get(alias_name) orelse {
try p.lexer.addDefaultError("Undefined alias");
return error.SyntaxError;
};
},
// Handle scalar values
.t_false => brk: {
try p.lexer.next();
break :brk p.e(E.Boolean{ .value = false }, loc);
},
.t_true => brk: {
try p.lexer.next();
break :brk p.e(E.Boolean{ .value = true }, loc);
},
.t_null => brk: {
try p.lexer.next();
break :brk p.e(E.Null{}, loc);
},
// Handle quoted strings: "quoted" or 'quoted'
.t_string_literal => brk: {
const str_loc = p.lexer.loc();
const str = p.lexer.toString(str_loc);
try p.lexer.next();
break :brk str;
},
// Handle unquoted scalars: plain_text
.t_identifier => brk: {
const str = E.String{ .data = p.lexer.identifier };
try p.lexer.next();
break :brk p.e(str, loc);
},
// Handle numbers: 123, 3.14, -17
.t_numeric_literal => brk: {
const value = p.lexer.number;
try p.lexer.next();
break :brk p.e(E.Number{ .value = value }, loc);
},
.t_dash => {
p.lexer.addError(loc.toUsize(), "Unexpected array element. Try either adding an indentation, or wrapping in quotes", .{});
return error.SyntaxError;
},
// Handle flow sequences (bracket-based)
// Example: [item1, item2, item3]
.t_open_bracket => brk: {
try p.lexer.next();
var items = std.ArrayList(Expr).init(p.allocator);
errdefer items.deinit();
while (p.lexer.token != .t_close_bracket) {
if (items.items.len > 0) {
if (p.lexer.token != .t_comma) break;
try p.lexer.next();
// Handle newlines after commas
while (p.lexer.token == .t_newline) {
try p.lexer.next();
}
}
try items.append(try p.parseValue());
}
while (p.lexer.token == .t_newline) {
try p.lexer.next();
}
try p.lexer.expect(.t_close_bracket);
break :brk p.e(E.Array{
.items = ExprNodeList.fromList(items),
.is_single_line = true,
}, loc);
},
// Handle flow mappings (brace-based)
// Example: {key1: value1, key2: value2}
.t_open_brace => brk: {
try p.lexer.next();
// Handle newlines before the first key
while (p.lexer.token == .t_newline) {
try p.lexer.next();
}
const expr = p.e(E.Object{}, loc);
const obj = expr.data.e_object;
while (p.lexer.token != .t_close_brace) {
if (obj.properties.len > 0) {
if (p.lexer.token != .t_comma) break;
try p.lexer.next();
// Handle newlines after commas
while (p.lexer.token == .t_newline) {
try p.lexer.next();
}
}
const key = try p.parseKey(p.allocator);
const key_loc = p.lexer.loc();
try p.lexer.expect(.t_colon);
const value = try p.parseValue();
obj.setRope(key, p.allocator, value) catch |err| {
switch (err) {
error.Clobber => {
// TODO: add key name.
p.lexer.addError(key_loc.toUsize(), "Cannot redefine key", .{});
return error.SyntaxError;
},
else => return err,
}
};
// Handle newlines after values
while (p.lexer.token == .t_newline) {
try p.lexer.next();
}
}
while (p.lexer.token == .t_newline) {
try p.lexer.next();
}
try p.lexer.expect(.t_close_brace);
break :brk expr;
},
else => {
try p.lexer.unexpected();
return error.SyntaxError;
},
};
// Process anchors - store the value for later reference
// Example: &anchor value -> stores value under name "anchor"
if (p.lexer.current_anchor) |anchor_name| {
p.lexer.current_anchor = null;
try p.lexer.anchors.put(anchor_name, value);
}
// Process tags - convert values based on type tags
// Examples:
// !!str "123" -> string "123"
// !!int "123" -> number 123
// !!bool "true" -> boolean true
// !!null "" -> null
if (p.lexer.current_tag) |tag| {
if (strings.eqlComptime(tag, "!!str")) {
// Already a string, no conversion needed
} else if (strings.eqlComptime(tag, "!!int")) {
if (value.data == .e_string) {
const int_val = std.fmt.parseInt(i64, value.data.e_string.data, 10) catch {
try p.lexer.addDefaultError("Invalid integer value");
return error.SyntaxError;
};
value = p.e(E.Number{ .value = @as(f64, @floatFromInt(int_val)) }, loc);
}
} else if (strings.eqlComptime(tag, "!!float")) {
if (value.data == .e_string) {
const float_val = std.fmt.parseFloat(f64, value.data.e_string.data) catch {
try p.lexer.addDefaultError("Invalid float value");
return error.SyntaxError;
};
value = p.e(E.Number{ .value = float_val }, loc);
}
} else if (strings.eqlComptime(tag, "!!bool")) {
if (value.data == .e_string) {
const bool_val = if (strings.eqlComptime(value.data.e_string.data, "true"))
true
else if (strings.eqlComptime(value.data.e_string.data, "false"))
false
else {
try p.lexer.addDefaultError("Invalid boolean value");
return error.SyntaxError;
};
value = p.e(E.Boolean{ .value = bool_val }, loc);
}
} else if (strings.eqlComptime(tag, "!!null")) {
value = p.e(E.Null{}, loc);
}
p.lexer.current_tag = null;
}
// Handle any trailing newlines after the value
while (p.lexer.token == .t_newline) {
try p.lexer.next();
}
return value;
}
};