");
- var it = std.mem.tokenizeScalar(u8, t.advanceLine()[1..], '|');
+ var it = mem.tokenizeScalar(u8, t.advanceLine()[1..], '|');
while (it.next()) |tok| {
try writer.writeAll("");
try processInlineMarkdown(gpa, tok, writer);
@@ -282,7 +497,7 @@ fn processMarkdown(gpa: std.mem.Allocator, src: []const u8, compiler_path: []con
pub fn nextInlineToken(src: []const u8) ?struct { InlineType, usize } {
var pos: usize = 0;
while (true) : (pos += 1) {
- pos = std.mem.indexOfAnyPos(u8, src, pos, "*`") orelse return null;
+ pos = mem.indexOfAnyPos(u8, src, pos, "*`[") orelse return null;
switch (src[pos]) {
'*' => switch (src[pos + 1]) {
@@ -290,12 +505,13 @@ pub fn nextInlineToken(src: []const u8) ?struct { InlineType, usize } {
else => continue,
},
'`' => return .{ .code, pos },
+ '[' => return .{ .link, pos },
else => unreachable,
}
}
}
-fn processInlineMarkdown(gpa: std.mem.Allocator, src: []const u8, writer: anytype) !void {
+fn processInlineMarkdown(gpa: Allocator, src: []const u8, writer: anytype) !void {
var bold = false;
var remain = src;
@@ -312,13 +528,32 @@ fn processInlineMarkdown(gpa: std.mem.Allocator, src: []const u8, writer: anytyp
remain = remain[2..];
},
.code => {
- const end = std.mem.indexOfScalarPos(u8, remain, 1, '`') orelse @panic("invalid code block");
+ const end = mem.indexOfScalarPos(u8, remain, 1, '`') orelse @panic("invalid code block");
const code = remain[1..end];
try writer.writeAll("");
try writeTokenizedCode(gpa, code, writer);
try writer.writeAll("");
remain = remain[end + 1 ..];
},
+ .link => {
+ const label_end = mem.indexOfScalarPos(u8, remain, 1, ']') orelse @panic("invalid link block");
+ assert(remain[label_end + 1] == '('); // invalid link. '[' syntax is not implemented
+
+ const href_end = mem.indexOfScalarPos(u8, remain, label_end + 2, ')') orelse @panic("invalid link block");
+
+ const label = remain[1..label_end];
+ const href = remain[label_end + 2 .. href_end];
+
+ try writer.writeAll("");
+ try processInlineMarkdown(gpa, label, writer);
+ try writer.writeAll("");
+ remain = remain[href_end + 1 ..];
+ },
}
}
try writer.writeAll(remain);
@@ -330,7 +565,7 @@ fn processInlineMarkdown(gpa: std.mem.Allocator, src: []const u8, writer: anytyp
// TODO: Change this to use Tree-sitter, which would also make it very simple
// to highlight JavaScript code in addition to the language.
-fn writeTokenizedCode(gpa: std.mem.Allocator, code: []const u8, writer: anytype) !void {
+fn writeTokenizedCode(gpa: Allocator, code: []const u8, writer: anytype) !void {
const code_z = try gpa.dupeZ(u8, code);
defer gpa.free(code_z);
@@ -344,7 +579,7 @@ fn writeTokenizedCode(gpa: std.mem.Allocator, code: []const u8, writer: anytype)
t.pos += 1;
continue;
} orelse break;
- try writer.writeAll(t.code[last..tok.offset]);
+ try writeHtmlEscapedSlice(t.code[last..tok.offset], writer, false);
try writer.print("{s}", .{
@tagName(tok.tag),
if (tok.tag.isKeyword()) " kw" else "",
@@ -352,17 +587,18 @@ fn writeTokenizedCode(gpa: std.mem.Allocator, code: []const u8, writer: anytype)
});
last = t.pos;
}
+ try writeHtmlEscapedSlice(t.code[last..], writer, false);
}
-fn writeAnsi(gpa: std.mem.Allocator, code: []const u8, writer: anytype) !void {
+fn writeAnsi(gpa: Allocator, code: []const u8, writer: anytype) !void {
var remain = code;
var count: usize = 0;
- while (std.mem.indexOfScalar(u8, remain, '\x1b')) |esc_i| {
- try writer.writeAll(remain[0..esc_i]);
+ while (mem.indexOfScalar(u8, remain, '\x1b')) |esc_i| {
+ try writeHtmlEscapedSlice(remain[0..esc_i], writer, false);
switch (remain[esc_i + 1]) {
'[' => switch (remain[esc_i + 2]) {
'0'...'9' => {
- const end = std.mem.indexOfScalarPos(u8, remain, esc_i + 2, 'm').?;
+ const end = mem.indexOfScalarPos(u8, remain, esc_i + 2, 'm').?;
const int = std.fmt.parseInt(u8, remain[esc_i + 2 .. end], 10) catch unreachable;
switch (int) {
else => {
@@ -385,3 +621,88 @@ fn writeAnsi(gpa: std.mem.Allocator, code: []const u8, writer: anytype) !void {
}
_ = gpa;
}
+
+fn nameToHeaderId(alloc: Allocator, input: []const u8) ![]const u8 {
+ var out = try ArrayList(u8).initCapacity(alloc, input.len);
+
+ var is_split = false;
+ for (input) |char| {
+ switch (char) {
+ 'a'...'z', 'A'...'Z', '0'...'9' => {
+ if (is_split) {
+ out.appendAssumeCapacity('-');
+ is_split = false;
+ }
+ out.appendAssumeCapacity(std.ascii.toLower(char));
+ },
+ else => {
+ is_split = true;
+ },
+ }
+ }
+
+ return try out.toOwnedSlice();
+}
+
+fn replaceInTemplate(alloc: Allocator, template: []const u8, replacements: anytype) ![]const u8 {
+ const Replacements = @TypeOf(replacements);
+ const Map = std.StringArrayHashMapUnmanaged([]const u8);
+
+ var map: Map = .{};
+ inline for (@typeInfo(Replacements).Struct.fields) |field| {
+ try map.put(alloc, field.name, @field(replacements, field.name));
+ }
+
+ var out: ArrayListUnmanaged(u8) = .{};
+ const w = out.writer(alloc);
+
+ var i: usize = 0;
+ while (mem.indexOfScalarPos(u8, template, i, '{')) |open_at| {
+ const end = mem.indexOfScalarPos(u8, template, open_at + 1, '}') orelse break;
+ const inner = template[open_at + 1 .. end];
+
+ if (map.get(inner)) |replacement| {
+ // write previous
+ try w.writeAll(template[i..open_at]);
+ // write template
+ try w.writeAll(replacement);
+ // move one past the ending '}'
+ i = end + 1;
+ } else {
+ // write up to and including the {
+ try w.writeAll(template[i .. open_at + 1]);
+ // move one past it
+ i = open_at + 1;
+ }
+ }
+
+ // write ending
+ try w.writeAll(template[i..]);
+
+ return out.items;
+}
+
+fn writeHtmlEscapedSlice(text: []const u8, writer: anytype, escape_quotes: bool) !void {
+ var i: usize = 0;
+ while (mem.indexOfAnyPos(
+ u8,
+ text,
+ i,
+ if (escape_quotes)
+ &.{ '&', '<', '>', '"' }
+ else
+ &.{ '&', '<', '>' },
+ )) |at| {
+ const replace = switch (text[at]) {
+ '&' => "&",
+ '<' => "<",
+ '>' => ">",
+ '"' => """,
+ else => unreachable,
+ };
+ try writer.writeAll(text[i..at]);
+ try writer.writeAll(replace);
+ i = at + 1;
+ }
+ try writer.writeAll(text[i..]);
+}
diff --git a/test/behavior.zig b/test/behavior.zig
new file mode 100644
index 0000000000000000000000000000000000000000..373f1d4ea5ec37a852c51a4b679c12c120124d59
--- /dev/null
+++ b/test/behavior.zig
@@ -0,0 +1,393 @@
+//! This program executes behavior tests found in ./test/behavior. Additionally, it can be run
+//! as a standalone program to compile and execute a single behavior test (for debugging)
+
+/// This file is included for all behavior tests for the JavaScript backend.
+const helpers =
+ \\(function (self) {
+ \\ function assert(condition) {
+ \\ if (!condition) {
+ \\ throw new Error("Assertion failure");
+ \\ }
+ \\ }
+ \\
+ \\ function assertEqual(actual, expected) {
+ \\ if (actual !== expected) {
+ \\ var err;
+ \\ try {
+ \\ err = new Error("Expected " + expected + ", but got " + actual);
+ \\ } catch (e) {
+ \\ err = new Error("Assertion failure");
+ \\ }
+ \\ throw err;
+ \\ }
+ \\ }
+ \\
+ \\ function print(str) {
+ \\ console.log(str);
+ \\ }
+ \\
+ \\ self.assert = assert;
+ \\ self.assertEqual = assertEqual;
+ \\ self.print = print;
+ \\})(typeof globalThis !== "undefined" ? globalThis : this);
+ \\
+;
+
+const CaseOptions = struct {
+ dump_ir: bool = false,
+ sync: bool = false,
+};
+
+const Case = struct {
+ path: []const u8,
+ display: []const u8,
+ success: bool = false,
+ logs: []const u8 = "",
+ failure: ?struct {
+ err: anyerror,
+ stack: core.StoredTrace,
+ } = null,
+};
+
+fn runTestCase(
+ backing_allocator: std.mem.Allocator,
+ file_path: []const u8,
+ case_options: CaseOptions,
+ out: std.io.AnyWriter,
+) !void {
+ var gpa = std.heap.GeneralPurposeAllocator(.{
+ .stack_trace_frames = 100,
+ }){ .backing_allocator = backing_allocator };
+ const alloc = gpa.allocator();
+
+ var backend = core.Js{};
+ var comp: core.Comp = .{
+ .gpa = tracer.tracingAllocator(&alloc),
+ .options = .{ .dump_ir = case_options.dump_ir },
+ .backend = .{ .js = &backend },
+ .out = .{ .writer = out, .tty = .escape_codes },
+ };
+ try comp.init();
+
+ const main_file = try comp.getFile(file_path);
+
+ var fail_report: core.Report = undefined;
+ const teg = main_file.getTeg(&comp, &fail_report) catch |err| switch (err) {
+ error.TransformFail => {
+ try out.print("Transform failed:\n", .{});
+ try fail_report.dumpToWriter(&comp, out, .escape_codes);
+ return error.TransformFail;
+ },
+ error.ParseFail => {
+ // Already printed.
+ return error.ParseFail;
+ },
+ else => return err,
+ };
+
+ if (case_options.dump_ir) {
+ try teg.dumpToWriter(out, .escape_codes);
+ }
+
+ const entry_point = try comp.semaEntryPoint(main_file);
+
+ if (comp.temp_flag_had_errors)
+ return error.SemaErrors;
+
+ if (teg.report.warnings.len > 0)
+ return error.TegWarnings;
+
+ var js_out: std.ArrayListUnmanaged(u8) = .{};
+ defer js_out.deinit(alloc);
+
+ try js_out.appendSlice(alloc, helpers);
+ try js_out.appendSlice(alloc, "\n\n(function() {\n\n");
+
+ const start_i = js_out.items.len;
+ try backend.link(&comp, entry_point, js_out.writer(alloc).any());
+
+ try out.writeAll("--- JS CODE ---\n");
+ try out.writeAll(js_out.items[start_i..]);
+ try out.writeAll("--- JS CODE ---\n");
+
+ try js_out.appendSlice(alloc, "\n\n})();");
+ try js_out.append(alloc, 0);
+
+ const runtime = qjs.JS_NewRuntime() orelse
+ return error.UnexpectedError;
+ defer qjs.JS_FreeRuntime(runtime);
+ const js_ctx = qjs.JS_NewContext(runtime) orelse
+ return error.UnexpectedError;
+ defer qjs.JS_FreeContext(js_ctx);
+
+ const result = qjs.JS_Eval(
+ js_ctx,
+ js_out.items.ptr,
+ js_out.items.len - 1,
+ "test.js",
+ qjs.JS_EVAL_TYPE_GLOBAL,
+ );
+ defer qjs.JS_FreeValue(js_ctx, result);
+
+ if (qjs.JS_IsException(result) != 0) {
+ const exception = qjs.JS_GetException(js_ctx);
+ defer qjs.JS_FreeValue(js_ctx, exception);
+
+ const exception_str = qjs.JS_ToCString(js_ctx, exception);
+ defer qjs.JS_FreeCString(js_ctx, exception_str);
+
+ const slice = std.mem.span(exception_str);
+
+ try out.print("Exception: {s}\n", .{slice});
+
+ return error.RuntimeFailed;
+ }
+}
+
+const threadlocal_state = struct {
+ threadlocal var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
+};
+
+pub fn runTestCaseWrap(
+ gpa: std.mem.Allocator,
+ case: *Case,
+ case_options: CaseOptions,
+ progress: std.Progress.Node,
+) void {
+ const node = progress.start(case.display, 0);
+ defer node.end();
+
+ const alloc = threadlocal_state.arena.allocator();
+ defer _ = threadlocal_state.arena.reset(.{ .retain_with_limit = std.mem.page_size * 8 });
+
+ var logs: std.ArrayListUnmanaged(u8) = .{};
+ defer logs.deinit(alloc);
+
+ defer case.logs = gpa.dupe(u8, logs.items) catch
+ "[logs failed to allocate]";
+
+ if (case_options.sync) std.debug.lockStdErr();
+ defer if (case_options.sync) std.debug.unlockStdErr();
+
+ runTestCase(
+ alloc,
+ case.path,
+ case_options,
+ if (case_options.sync)
+ std.io.getStdErr().writer().any()
+ else
+ logs.writer(alloc).any(),
+ ) catch |err| {
+ const stack = switch (err) {
+ error.TransformFail,
+ error.ParseFail,
+ error.TegWarnings,
+ error.SemaErrors,
+ error.AnalysisFail,
+ => core.StoredTrace.empty,
+
+ else => core.StoredTrace.from(@errorReturnTrace()),
+ };
+
+ case.failure = .{
+ .err = err,
+ .stack = stack,
+ };
+
+ return;
+ };
+
+ case.success = true;
+}
+
+pub fn main() !void {
+ // unicode props is asserted to be initialized before core.Tokenizer is used
+ // this will result in a static page allocation, which is never cleaned up.
+ try core.unicode_props.init();
+ var gpa = std.heap.GeneralPurposeAllocator(.{
+ .stack_trace_frames = 100,
+ .thread_safe = true,
+ }){};
+
+ defer _ = gpa.deinit();
+ const alloc = gpa.allocator();
+
+ const args = try std.process.argsAlloc(alloc);
+ defer std.process.argsFree(alloc, args);
+
+ var test_root: []const u8 = "test/behavior";
+ var case_options: CaseOptions = .{};
+ var all_logs = false;
+ var filter: ?[]const u8 = null;
+ defer if (filter) |f| alloc.free(f);
+
+ var argument_error = false;
+
+ for (args[1..]) |full_arg| {
+ if (std.mem.startsWith(u8, full_arg, "--")) {
+ if (getFlag("--test-root", full_arg)) |flag| {
+ test_root = flag;
+ } else if (std.mem.eql(u8, "--dump-ir", full_arg)) {
+ case_options.dump_ir = true;
+ } else if (std.mem.eql(u8, "--all-logs", full_arg)) {
+ all_logs = true;
+ } else if (std.mem.eql(u8, "--sync", full_arg)) {
+ case_options.sync = true;
+ } else {
+ std.debug.print("Unknown argument: {s}\n", .{full_arg});
+ argument_error = true;
+ }
+ } else {
+ if (filter != null) {
+ std.debug.print("Unknown positional argument: {s}\n", .{full_arg});
+ } else {
+ filter = try std.mem.replaceOwned(
+ u8,
+ alloc,
+ full_arg,
+ if (std.fs.path.sep == '/') "\\" else '/',
+ std.fs.path.sep_str,
+ );
+ }
+ }
+ }
+
+ if (argument_error) std.process.exit(1);
+
+ const root = try std.fs.cwd().openDir(test_root, .{ .iterate = true });
+
+ var test_cases: std.ArrayListUnmanaged(Case) = .{};
+ defer test_cases.deinit(alloc);
+ defer for (test_cases.items) |test_case| {
+ alloc.free(test_case.path);
+ };
+
+ var path_buf: std.BoundedArray(u8, std.fs.max_path_bytes) = .{};
+ path_buf.len = @intCast((try std.os.getFdPath(root.fd, &path_buf.buffer)).len);
+ try iterateTestCases(alloc, root, path_buf.len, &path_buf, &test_cases, filter);
+
+ if (test_cases.items.len == 0) {
+ if (filter) |f| {
+ std.debug.print("No test cases match filter \"{s}\"\n", .{f});
+ } else {
+ std.debug.print("No test cases\n", .{});
+ }
+ std.process.exit(1);
+ }
+
+ const progress = std.Progress.start(.{ .root_name = "Behavior Tests" });
+ defer progress.end();
+ progress.setEstimatedTotalItems(test_cases.items.len);
+
+ var wg: std.Thread.WaitGroup = .{};
+
+ var pool: std.Thread.Pool = undefined;
+ if (!case_options.sync) try pool.init(.{ .allocator = alloc });
+ defer if (!case_options.sync) pool.deinit();
+
+ for (test_cases.items) |*test_case| {
+ const call_args = .{
+ alloc,
+ test_case,
+ case_options,
+ progress,
+ };
+
+ if (case_options.sync) {
+ @call(.auto, runTestCaseWrap, call_args);
+ } else {
+ pool.spawnWg(&wg, runTestCaseWrap, call_args);
+ }
+ }
+
+ wg.wait();
+
+ for (test_cases.items) |*case| {
+ if (case.success) {
+ if (all_logs) {
+ std.debug.print("PASS: {s}\n", .{case.display});
+ std.debug.print("{s}", .{case.logs});
+ }
+ } else if (case.failure) |*fail| {
+ std.debug.print("FAIL: {s}\n", .{case.display});
+ std.debug.print("{s}", .{case.logs});
+ if (fail.stack.index > 0) {
+ std.debug.print("Error return trace:\n", .{});
+ std.debug.dumpStackTrace(fail.stack.trace());
+ }
+ } else {
+ // success was never set true but failure was left null
+ // this means runTestCaseWrap was never called
+ std.debug.print("INVALID STATE: {s}\n", .{case.path});
+ }
+ if (case.logs.len > 0)
+ alloc.free(case.logs);
+ }
+}
+
+fn iterateTestCases(
+ alloc: std.mem.Allocator,
+ root: Dir,
+ root_path_len: usize,
+ path_buf: *std.BoundedArray(u8, std.fs.max_path_bytes),
+ test_cases: *std.ArrayListUnmanaged(Case),
+ filter: ?[]const u8,
+) !void {
+ var it = root.iterate();
+ while (try it.next()) |entry| {
+ switch (entry.kind) {
+ .file => {
+ if (entry.name.len > 0 and entry.name[0] == '.')
+ continue;
+
+ const path = try std.fs.path.join(alloc, &.{
+ path_buf.slice(),
+ entry.name,
+ });
+
+ if (filter) |f| {
+ if (std.mem.indexOf(u8, path, f) == null) {
+ alloc.free(path);
+ continue;
+ }
+ }
+
+ try test_cases.append(alloc, .{
+ .path = path,
+ .display = path[root_path_len + 1 ..],
+ });
+ },
+ .directory => {
+ const top = path_buf.len;
+ defer path_buf.len = top;
+
+ try path_buf.append(std.fs.path.sep);
+ try path_buf.appendSlice(entry.name);
+
+ const sub_dir = try root.openDir(entry.name, .{ .iterate = true });
+ try iterateTestCases(alloc, sub_dir, root_path_len, path_buf, test_cases, filter);
+ },
+ else => return error.UnsupportedFileType,
+ }
+ }
+}
+
+fn getFlag(name: []const u8, full_arg: []const u8) ?[]const u8 {
+ if (full_arg.len > name.len + 1 and
+ full_arg[name.len] == '=' and
+ std.mem.startsWith(u8, name, full_arg))
+ {
+ return full_arg[name.len + 1 ..];
+ }
+
+ return null;
+}
+
+const std = @import("std");
+const Dir = std.fs.Dir;
+
+const core = @import("core");
+const qjs = @import("quickjs");
+
+const tracer = @import("tracer");
+pub const tracer_impl = @field(tracer, @import("build_options").tracing);
diff --git a/test/behavior/arithmetic b/test/behavior/arithmetic
new file mode 100644
index 0000000000000000000000000000000000000000..59a036d83dd160c3025a3258a29f683a93bd6b12
--- /dev/null
+++ b/test/behavior/arithmetic
@@ -0,0 +1,67 @@
+fn f(a: float) -> float { a }
+
+fn addFloat(a: float, b: float) -> float {
+ a + b
+}
+
+fn subFloat(a: float, b: float) -> float {
+ a - b
+}
+
+fn mulFloat(a: float, b: float) -> float {
+ a * b
+}
+
+fn divFloat(a: float, b: float) -> float {
+ a / b
+}
+
+// fn modFloat(a: float, b: float) -> float {
+// a % b
+// }
+
+// fn expFloat(a: float, b: float) -> float {
+// a ** b
+// }
+
+fn i(a: int) -> int { a }
+
+fn addInt(a: int, b: int) -> int {
+ a + b
+}
+
+fn subInt(a: int, b: int) -> int {
+ a - b
+}
+
+// fn mulInt(a: int, b: int) -> int {
+// a * b
+// }
+
+// fn divInt(a: int, b: int) -> int {
+// a / b
+// }
+
+// fn expInt(a: int, b: int) -> int {
+// a * b
+// }
+
+// fn modFloat(a: float, b: float) -> float {
+// a % b
+// }
+
+let assertEqual = @js("assertEqual");
+
+assertEqual(addFloat(1, 2), f(3));
+assertEqual(addFloat(1.5, 2.2), f(3.7));
+assertEqual(subFloat(1, 2), f(-1));
+assertEqual(subFloat(1.5, 2.2), f(-0.7000000000000002));
+assertEqual(mulFloat(1, 2), f(2));
+assertEqual(mulFloat(1.5, 2.2), f(3.3000000000000003));
+assertEqual(divFloat(1, 2), f(0.5));
+assertEqual(divFloat(1.5, 2.2), f(0.6818181818181818));
+
+assertEqual(addInt(1, 2), i(3));
+assertEqual(addInt(-421, 592), i(171));
+assertEqual(subInt(1, 2), i(-1));
+assertEqual(subInt(528, -43), i(571));
|