diff --git a/.gitignore b/.gitignore
index e398ea9..ed25003 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,4 @@
.build/
**.dSYM
.DS_Store
+docs/
diff --git a/STYLEGUIDE b/STYLEGUIDE
index 2d085c6..82c4b47 100644
--- a/STYLEGUIDE
+++ b/STYLEGUIDE
@@ -9,9 +9,10 @@ Getting Paid
------------
To pick up tasks, find something in TODO and message me
-your rate. If accepted, create a single commit moving it
-from the 'UP NEXT' section to your 'IN PROGRESS' section;
-the commit message should only say 'start [id]'.
+your rate (bonus points if your initials are "JC"). If
+accepted, create a single commit moving it from the 'UP
+NEXT' section to your 'IN PROGRESS' section; the commit
+message should only say 'start [id]'.
Once the work is done (use as many commits as you'd like),
create another commit moving the task from your 'IN
diff --git a/_generate_docs.jai b/_generate_docs.jai
new file mode 100644
index 0000000..fddb08a
--- /dev/null
+++ b/_generate_docs.jai
@@ -0,0 +1,1288 @@
+// *** Here be dragons! ***
+// This code is mostly a proof of concept that ended up working pretty well.
+// I'm sure there's a ton of edgecases we'll run into as the base library grows,
+// but it works for now.
+
+UseLocalLinks :: false; // set to false when deploying the docs
+
+#scope_file #run {
+ set_build_options_dc(.{ do_output = false });
+
+ ws := compiler_create_workspace();
+ opts := get_build_options(ws);
+ opts.output_type = .NO_OUTPUT;
+ set_build_options(opts, ws);
+
+ compiler_begin_intercept(ws);
+ add_build_string(#string END
+ // Add freestanding modules here.
+ // Other modules will be automatically generated if they're imported by someone else.
+
+ jc :: #import "jc";
+ hmm :: #import "jc/ext/hmm";
+ luajit :: #import "jc/ext/luajit";
+ raylib :: #import "jc/ext/raylib";
+ remotery :: #import "jc/ext/remotery";
+
+ // darwin :: #import "jc/ext/darwin";
+ // objc :: #import "jc/ext/objc";
+ END, ws);
+
+ CheckImport :: (import: *Code_Directive_Import) -> bool {
+ return (BelongsToJc(import) || contains(import.name, "jc")) && !(import.flags & .UNSHARED);
+ }
+
+ BuildImport :: (import: *Code_Directive_Import) -> Import {
+ return .{
+ node = import,
+ name = import.name,
+ external = !contains(import.name, "jc"),
+ };
+ }
+
+ while true {
+ msg := compiler_wait_for_message();
+ if msg.workspace != ws continue;
+ if msg.kind == .COMPLETE || msg.kind == .ERROR break;
+
+ if msg.kind == {
+ case .TYPECHECKED;
+ tc := msg.(*Message_Typechecked);
+ for tc.others if BelongsToJc(it.expression) {
+ if it.expression.kind == {
+ case .DIRECTIVE_IMPORT;
+ import := it.expression.(*Code_Directive_Import);
+ if CheckImport(import) {
+ name := ModuleName(import);
+ if name.count == 0 continue;
+
+ mod := GetModule(name);
+ array_add_if_unique(*mod.imports, BuildImport(import));
+ }
+
+ case .DIRECTIVE_MODULE_PARAMETERS;
+ }
+ }
+
+ for tc.declarations if BelongsToJc(it.expression) {
+ decl := it.expression;
+ if !(decl.flags & .IS_GLOBAL) continue;
+ if decl.flags & .SCOPE_FILE continue;
+
+ ignored := false;
+ for decl.notes if it.text == "jc.nodocs" {
+ ignored = true;
+ break;
+ }
+
+ // Ignore this decl if explicit instructed or
+ // if the expression isn't something we should document.
+ if ignored || !(ValidNode(decl.expression) && BelongsToJc(decl.expression))
+ { continue; }
+
+ name := ModuleName(decl);
+ if name.count == 0 continue;
+
+ mod := GetModule(name);
+
+ expr := decl.expression;
+ if expr.kind == {
+ case .STRUCT; #through;
+ case .ENUM;
+ array_add(*mod.types, .{
+ decl = decl,
+ node = expr,
+ source = CleanJaiSource(decl),
+ docs = DocComments(decl),
+ });
+
+ case .PROCEDURE_HEADER;
+ array := *mod.procs;
+ header := expr.(*Code_Procedure_Header);
+ if header.procedure_flags & .MACRO {
+ array = *mod.macros;
+ }
+
+ array_add(array, .{
+ decl = decl,
+ node = header,
+ source = CleanJaiSource(decl),
+ docs = DocComments(decl),
+ });
+
+ case;
+ if !(decl.flags & .IS_CONSTANT)
+ { continue; }
+
+ array := *mod.consts;
+
+ // Things we don't want to be constant decls
+ if expr.kind == {
+ case .TYPE_INSTANTIATION;
+ array = *mod.types;
+
+ case .IDENT;
+ ident := decl.expression.(*Code_Ident);
+ expr := ident.resolved_declaration.expression;
+ if expr.kind == .TYPE_DEFINITION {
+ array = *mod.types;
+ }
+
+ case .DIRECTIVE_IMPORT;
+ import := expr.(*Code_Directive_Import);
+ if CheckImport(import) {
+ array_add_if_unique(*mod.imports, BuildImport(import));
+ }
+
+ continue;
+ }
+
+ array_add(array, .{
+ decl = decl,
+ node = expr,
+ source = CleanJaiSource(decl),
+ docs = DocComments(decl),
+ });
+ }
+ }
+ }
+ }
+
+ compiler_end_intercept(ws);
+
+ print("generating documentation...\n");
+
+ // Render each module page
+ for * mod, mod_name: modules {
+ print("processing module '%'...\n", mod_name);
+ ok := write_entire_file(tprint("docs/%", LinkableName(mod_name)), ModuleToHtml(mod));
+ assert(ok, "failed to write documentation for module: %", mod_name);
+ }
+
+ // Render index.html
+ b: String_Builder;
+ StartPage(*b, tprint("Jc v%.% Documentation", JcMajor, JcMinor));
+ print_to_builder(*b, "
Jc v%.% Documentation
", JcMajor, JcMinor);
+
+ names: [..]string;
+ for _, mod_name: modules {
+ array_add(*names, mod_name);
+ }
+
+ contributors :: string.[
+ "Judah Caruso",
+ "Jesse Coyle",
+ ];
+
+ print_to_builder(*b, #string END
+
+ License: Public Domain
+
+ Repository: %2
+
+ Contributors: %3
+
+ END,
+ "https://git.brut.systems/judah/jc/src/branch/master/LICENSE",
+ "https://git.brut.systems/judah/jc",
+ join(..contributors, ", "),
+ );
+
+ append(*b, #string END
+
+ Modules
+
+ END);
+ for quick_sort(names, SortAlphabeticallyDescendingLength) {
+ print_to_builder(*b, "- %2
", LinkableName(it), it);
+ }
+
+ append(*b, #string END
+
+
+ END);
+
+ append(*b, #string END
+
+ What:
+ A set of modules for things I usually want in Jai, namely, utilities, bindings, and experiments.
+
+
+
+ Why:
+ Because Jai is still in closed beta (as of September, 2025),
+ updates to the compiler and included modules break
+ projects of mine; sometimes in very annoying ways.
+
+
+ Jc was made to 1) give myself an escape hatch/skin-suit to
+ cause fewer breaking changes when updating the compiler,
+ and 2) put all of my non-project code in a single place
+ that's easier to manage.
+
+
+ While I do use many of the modules shipped with the
+ compiler, my goal is to eventually replace them.
+
+
+
+ How:
+
+# Direct installation
+cd [jai install dir]/modules
+git clone https://git.brut.systems/judah/jc.git
+
+# Indirect installation
+git clone https://git.brut.systems/judah/jc.git
+
+ln -s "/path/to/jc" [jai install dir]/modules/jc # POSIX install
+mklink /D "C:\path\to\jc" [jai install dir]\jc # Windows install
+
+# Usage:
+#import "jc";
+#import "jc/[module]";
+
+
+ END);
+
+ EndPage(*b);
+
+ ok := write_entire_file("docs/index.html", builder_to_string(*b));
+ assert(ok, "failed to write docs/index.html");
+
+ print("done!\n");
+}
+
+source_files: Table(string, []string);
+
+GetSourceLines :: (filename: string) -> []string, bool {
+ // old_logger := context.logger;
+ // context.logger = (message: string, data: *void, info: Log_Info) {}; // do nothing logger
+ // defer context.logger = old_logger;
+
+ fok, src := table_find_new(*source_files, filename);
+ if fok {
+ return src, true;
+ }
+
+
+ data, rok := read_entire_file(filename);
+ lines: []string;
+ if rok {
+ lines = split(data, "\n");
+ table_add(*source_files, filename, lines);
+ }
+
+ return lines, rok;
+}
+
+modules: Table(string, Module);
+
+GetModule :: (name: string) -> *Module {
+ mod := find_or_add(*modules, name);
+ mod.name = name;
+
+ if mod.main_docs.count != 0
+ { return mod; }
+
+ lines, _ := GetSourceLines(tprint(".%/module.jai", string.{ data = name.data + 2, count = name.count - 2 }));
+ if lines.count == 0 {
+ mod.main_docs = " "; // non-empty so we don't try again
+ return mod;
+ }
+
+// @todo(judah): this should handle preformatted text
+ cleaned_lines: [..]string;
+ blockbased := starts_with(lines[0], "/*");
+ linebased := starts_with(lines[0], "//") ;
+ for lines {
+ if blockbased {
+ if trim(it) == "*/"
+ { break; }
+ }
+ else if linebased {
+ if !starts_with(it, "/")
+ { break; }
+ }
+ else {
+ break;
+ }
+
+ line := trim_right(trim_left(it, "/*"));
+ if line.count > 0 {
+ array_add(*cleaned_lines, line);
+ }
+ }
+
+ mod.main_docs = join(..cleaned_lines, "\n");
+ return mod;
+}
+
+Decl :: struct(T: Type) {
+ using decl: *Code_Declaration;
+ node: *T;
+ source: string;
+ docs: string;
+}
+
+Import :: struct {
+ node: *Code_Directive_Import;
+ name: string;
+ external: bool;
+}
+
+operator== :: (a: Import, b: Import) -> bool {
+ return a.name == b.name;
+}
+
+Module :: struct {
+ name: string;
+ main_docs: string;
+ imports: [..]Import; // module import names
+ types: [..]Decl(Code_Node);
+ consts: [..]Decl(Code_Node);
+ procs: [..]Decl(Code_Procedure_Header);
+ macros: [..]Decl(Code_Procedure_Header);
+}
+
+LinkableName :: (mod_name: string) -> string {
+ return tprint("%.html", to_lower_copy(replace(mod_name, "/", "_")));
+}
+
+ModuleToHtml :: (mod: *Module) -> string {
+ Section :: (b: *String_Builder, name: string, decls: []Decl) -> bool {
+ print_to_builder(b, "", to_lower_copy(name), name);
+
+ if decls.count != 0 {
+ return true;
+ }
+
+ append(b, "This section is empty.
");
+ return false;
+ }
+
+ sorted_consts := quick_sort(mod.consts, SortDeclsByName);
+ sorted_procs := quick_sort(mod.procs, SortDeclsByName);
+ sorted_macros := quick_sort(mod.macros, SortDeclsByName);
+ sorted_types := quick_sort(mod.types, SortDeclsByName);
+ sorted_imports := quick_sort(mod.imports, SortImportsByName);
+
+ b: String_Builder;
+
+ import_name := mod.name;
+ short_name := path_basename(import_name);
+ StartPage(*b, tprint("Module % – Jc v%.% Documentation", import_name, JcMajor, JcMinor));
+ print_to_builder(*b, "[..] Module – %
", short_name);
+ print_to_builder(*b, "%
", mod.main_docs);
+ print_to_builder(*b, "#import \"%\";
", import_name);
+
+ PrintIndexFor :: (b: *String_Builder, name: string, decls: []Decl) {
+ append(b, "");
+ print_to_builder(b, "% (%)
", name, decls.count);
+ append(b, "");
+ for decls {
+ print_to_builder(b, "- %1
", it.name);
+ }
+ append(b, "
");
+ append(b, " ");
+ }
+
+ append(*b, "");
+ append(*b, "
");
+
+ if sorted_consts.count PrintIndexFor(*b, "Constants", sorted_consts);
+ if sorted_procs.count PrintIndexFor(*b, "Procedures", sorted_procs);
+ if sorted_macros.count PrintIndexFor(*b, "Macros", sorted_macros);
+ if sorted_types.count PrintIndexFor(*b, "Types", sorted_types);
+
+ if sorted_imports.count {
+ append(*b, "
");
+ print_to_builder(*b, "Imports (%)
", sorted_imports.count);
+ append(*b, "");
+ for sorted_imports {
+ append(*b, "- %
", it.name);
+ }
+ append(*b, "
");
+ append(*b, " ");
+ }
+
+ append(*b, "
");
+
+ append(*b, "");
+ if Section(*b, "Constants", sorted_consts) {
+ for sorted_consts {
+ append(*b, "
");
+ ConstantToHtml(*b, it);
+ append(*b, "
");
+ }
+ }
+ append(*b, "
");
+
+ append(*b, "");
+ if Section(*b, "Procedures", sorted_procs) {
+ for sorted_procs {
+ append(*b, "
");
+ ProcedureOrMacroToHtml(*b, it);
+ append(*b, "
");
+ }
+ }
+ append(*b, "
");
+
+ append(*b, "");
+ if Section(*b, "Macros", sorted_macros) {
+ for sorted_macros {
+ append(*b, "
");
+ ProcedureOrMacroToHtml(*b, it);
+ append(*b, "
");
+ }
+ }
+ append(*b, "
");
+
+ append(*b, "");
+ if Section(*b, "Types", sorted_consts) {
+ for sorted_types {
+ append(*b, "
");
+ TypeToHtml(*b, it);
+ append(*b, "
");
+ }
+ }
+ append(*b, "
");
+
+ EndPage(*b);
+ return builder_to_string(*b);
+}
+
+#if UseLocalLinks {
+ BaseUrl :: "docs";
+ SrcPath :: "..";
+}
+else {
+ BaseUrl :: "https://judahcaruso.com/jc";
+ SrcPath :: "https://git.brut.systems/judah/jc/src";
+}
+
+NodeUrl :: (node: *Code_Node) -> string {
+ path := node.enclosing_load.fully_pathed_filename;
+ idx := find_index_from_right(path, "jc/");
+ assert(idx != -1);
+ idx += 3;
+
+ return tprint("%1/%2#L%3",
+ SrcPath,
+ string.{ data = path.data + idx, count = path.count - idx },
+ node.l0,
+ );
+}
+
+DeclTitle :: (b: *String_Builder, decl: Decl) {
+ print_to_builder(b, #string END
+
+ %1
+ ¶
+
+ END, decl.name, NodeUrl(decl));
+
+ print_to_builder(b, "% :: %
", decl.name, decl.source);
+}
+
+WriteDocsAndCode :: (b: *String_Builder, docs: string, example: string) {
+ if docs.count != 0 {
+ append(b, "");
+ append(b, docs);
+ append(b, "
");
+ }
+
+ if example.count != 0 {
+ append(b, "");
+ append(b, "Example
");
+ append(b, "");
+ append(b, example);
+ append(b, "
");
+ append(b, " ");
+ }
+}
+
+ConstantToHtml :: (b: *String_Builder, decl: Decl) {
+ DeclTitle(b, decl);
+
+ doc_lines, example_lines := PrepareDocsAndCode(decl.docs);
+ docs := join(..doc_lines, "\n");
+ example := join(..example_lines, "\n");
+ WriteDocsAndCode(b, docs, example);
+}
+
+ProcedureOrMacroToHtml :: (b: *String_Builder, decl: Decl(Code_Procedure_Header)) {
+ DeclTitle(b, decl);
+
+ doc_lines, example_lines := PrepareDocsAndCode(decl.docs);
+ for line, li: doc_lines {
+ if starts_with(line, "Note:") {
+ doc_lines[li] = tprint("
%", line);
+ }
+
+ if starts_with(line, "See:") {
+ doc_lines[li] = tprint("
%", line);
+ }
+ }
+
+ // horrible
+ docs := join(..doc_lines, "\n");
+ for decl.node.arguments {
+ strs := [2]string.[
+ .[ tprint(" % ", it.name), tprint(" % ", it.name) ],
+ .[ tprint("%.", it.name), tprint("%.", it.name) ],
+ .[ tprint(",%", it.name), tprint(",%", it.name) ],
+ .[ tprint("%,", it.name), tprint("%,", it.name) ],
+ ];
+
+ for strs {
+ docs = replace(docs, it[0], it[1]);
+ }
+ }
+
+ example := join(..example_lines, "\n");
+ WriteDocsAndCode(b, docs, example);
+}
+
+TypeToHtml :: (b: *String_Builder, decl: Decl) {
+ DeclTitle(b, decl);
+ doc_lines, example_lines := PrepareDocsAndCode(decl.docs);
+ docs := join(..doc_lines, "\n");
+ example := join(..example_lines, "\n");
+ WriteDocsAndCode(b, docs, example);
+}
+
+PrepareDocsAndCode :: (raw_docs: string) -> (doc_lines: []string, example_lines: []string) {
+ if raw_docs.count == 0 {
+ return .[], .[];
+ }
+
+ lines := split(raw_docs, "\n");
+
+ doc_lines: [..]string;
+ example_lines: [..]string;
+
+ example_start: int = -1;
+
+ leading := 2;
+ for lines {
+ line := string.{ data = it.data + 3, count = it.count - 3 };
+ if line.count <= 0 {
+ array_add(*doc_lines, "");
+ continue;
+ }
+
+ if line[0] == " " {
+ line.data += 1;
+ line.count -= 1;
+ }
+
+ l := 0;
+ while l < line.count if line[l] == " " {
+ l += 1;
+ } else {
+ break;
+ }
+
+ if l > leading {
+ leading = l;
+ example_start = it_index;
+ break;
+ }
+
+ array_add(*doc_lines, trim(line));
+ }
+
+ if example_start != -1 {
+ for example_start..lines.count - 1 {
+ line := trim_right(lines[it]);
+ line.data += 3 + leading;
+ line.count -= 3 + leading;
+
+ if line.count <= 0 {
+ array_add(*example_lines, "");
+ continue;
+ }
+
+ if line[0] == " " {
+ line.data += 1;
+ line.count -= 1;
+ }
+
+ array_add(*example_lines, line);
+ }
+ }
+
+ return doc_lines, example_lines;
+}
+
+ModuleName :: (node: *Code_Node) -> string {
+ assert(node.enclosing_load != null);
+ assert(node.enclosing_load.enclosing_import != null);
+
+ import := node.enclosing_load.enclosing_import;
+ return import.module_name;
+}
+
+SortDeclsByName :: (a: Decl($T), b: Decl(T)) -> int {
+ return SortAlphabeticallyDescendingLength(a.name, b.name);
+}
+
+SortImportsByName :: (a: Import, b: Import) -> int {
+ if a.external && !b.external
+ { return 1; }
+
+ if b.external && !a.external
+ { return -1; }
+
+ return SortAlphabeticallyDescendingLength(a.name, b.name);
+}
+
+SortAlphabeticallyDescendingLength :: (a: string, b: string) -> int {
+ for 0..a.count-1 {
+ if it >= b.count
+ { return 1; }
+ delta := a[it].(int) - b[it].(int);
+ if delta
+ { return delta; }
+ }
+
+ if b.count > a.count
+ { return -1; }
+
+ return 0;
+}
+
+CleanJaiSource :: (node: *Code_Node) -> string {
+ assert(node.enclosing_load != null);
+ assert(node.kind == .DECLARATION);
+ decl := node.(*Code_Declaration);
+
+ lines, ok := GetSourceLines(node.enclosing_load.fully_pathed_filename);
+ assert(ok, "failed to get source: %", node.enclosing_load.fully_pathed_filename);
+
+ // for procedures, strip out anything that doesn't help documentation.
+ if decl.expression.kind == .PROCEDURE_HEADER {
+ line := trim(lines[node.l0-1]);
+
+ proc := decl.expression.(*Code_Procedure_Header);
+ has_ret := proc.returns.count != 0;
+
+ if has_ret {
+ arrow_idx := find_index_from_right(line, "->");
+ assert(arrow_idx != -1, "how?");
+
+ i := arrow_idx;
+ while i < line.count {
+ if line[i] == "#" || line[i] == "{" {
+ break;
+ }
+
+ i += 1;
+ }
+
+ line.count -= line.count - i;
+ }
+ else {
+ while line.count > 0 {
+ if line.data[line.count - 1] == ")" {
+ break;
+ }
+
+ line.count -= 1;
+ }
+ }
+
+
+ idx := find_index_from_left(line, "::");
+ assert(idx != -1);
+
+ head := string.{ data = line.data, count = idx };
+ tail := string.{ data = line.data + head.count, count = line.count - head.count };
+
+ while tail.count > 0 {
+ if tail[0] == "(" {
+ break;
+ }
+
+ tail.data += 1;
+ tail.count -= 1;
+ }
+
+ postfix: string;
+
+ header := decl.expression.(*Code_Procedure_Header);
+ if header.procedure_flags & .MACRO {
+ postfix = " #expand";
+ }
+
+ if header.procedure_flags & .COMPILE_TIME_ONLY {
+ postfix = tprint("% #compile_time", postfix);
+ }
+
+ return tprint("%1%2", trim(tail), postfix);
+ }
+ // for everything else, get the source range for the node and only strip out
+ // comments that would've been caught by the doc comment finder.
+ else {
+ GetRangeEnd :: (node: *Code_Node, prev_l1: int) -> int, *Code_Node {
+ if node.kind == {
+ case .STRUCT;
+ s := node.(*Code_Struct);
+ return s.l1, s.block;
+
+ case .ENUM;
+ e := node.(*Code_Enum);
+ return e.l1, e.block;
+
+ case .BLOCK;
+ return node.l1, null;
+
+ case;
+ // print("didn't handle: %\n", node.kind);
+ return node.l1, null;
+ }
+
+ return -1, null;
+ }
+
+ start := node.l0-1;
+ end := -1;
+
+ {
+ node := decl.expression;
+ while node != null {
+ e, n := GetRangeEnd(node, node.l1);
+ if n == null {
+ end = e;
+ break;
+ }
+
+ end = e;
+ node = n;
+ }
+ }
+
+ assert(end != -1);
+
+ cleaned_lines: [..]string;
+ for start..end - 1 {
+ line := lines[it];
+
+ inline_comment_idx := find_index_from_right(line, "///");
+ if inline_comment_idx != - 1 {
+ if !starts_with(trim(line), "///") {
+ line.count = inline_comment_idx;
+ }
+ }
+
+ array_add(*cleaned_lines, line);
+ }
+
+ source := join(..cleaned_lines, "\n");
+ index := find_index_from_left(source, "::");
+
+ // This is like 95% a module parameter
+ if index == -1 {
+ #import "Program_Print";
+ b: String_Builder;
+ if decl.expression != null {
+ print_expression(*b, decl.expression);
+ }
+ else if decl.type_inst {
+ print_expression(*b, decl.type_inst);
+ }
+
+ append(*b, "; // #module_parameter");
+ return builder_to_string(*b);
+ }
+
+ return trim(.{ data = source.data + (index + 2), count = source.count - (index + 2) });
+ }
+}
+
+DocComments :: (node: *Code_Node) -> string {
+ assert(node.enclosing_load != null);
+
+ all_lines, ok := GetSourceLines(node.enclosing_load.fully_pathed_filename);
+ assert(ok, "failed to get source: %", node.enclosing_load.fully_pathed_filename);
+
+ main_line := all_lines[node.l0-1];
+
+ inline_comment_idx := find_index_from_right(main_line, "///");
+ if inline_comment_idx != -1 {
+ return trim(.{
+ data = main_line.data + inline_comment_idx,
+ count = main_line.count - inline_comment_idx,
+ });
+ }
+
+ start := -1;
+ end := (node.l0-2).(int);
+
+ i := end;
+ while i >= 0 {
+ line := trim(all_lines[i]);
+ defer i -= 1;
+
+ if line.count == 0 || !starts_with(line, "///") {
+ start = i + 1;
+ break;
+ }
+ }
+
+ if start == -1 {
+ start = 0;
+ }
+
+ comment_lines: [..]string;
+ for start..end {
+ array_add(*comment_lines, trim(all_lines[it]));
+ }
+
+ return join(..comment_lines, "\n");
+}
+
+BelongsToJc :: (node: *Code_Node) -> bool {
+ if node.enclosing_load == null || node.enclosing_load.enclosing_import == null {
+ return false;
+ }
+
+ import := node.enclosing_load.enclosing_import;
+ return contains(import.module_name, "jc") && ValidNode(node);
+}
+
+ValidNode :: (node: *Code_Node) -> bool {
+ if node.kind == {
+ case .DECLARATION; #through;
+ case .DIRECTIVE_IMPORT; #through;
+ case .DIRECTIVE_RUN; #through;
+ case .PROCEDURE_HEADER; #through;
+ case .STRUCT; #through;
+ case .TYPE_DEFINITION; #through;
+ case .TYPE_INSTANTIATION; #through;
+ case .ENUM; #through;
+ case .LITERAL; #through;
+ case .IDENT; #through;
+ case .UNARY_OPERATOR; #through;
+ case .DIRECTIVE_MODULE_PARAMETERS; #through;
+ case .BINARY_OPERATOR;
+ return true;
+
+ case;
+ return false;
+ }
+}
+
+StartPage :: (b: *String_Builder, title: string) {
+ print_to_builder(b, #string END
+
+
+
+
+
+ %1
+
+
+
+
+
+ END, title, ResetCss, MainCss);
+}
+
+EndPage :: (b: *String_Builder) {
+ append(b, #string END
+
+ ↑
+
+
+ END);
+}
+
+
+MainCss :: #string END
+:root {
+ --bg-primary: #ffffff;
+ --text-primary: #333333;
+ --text-secondary: #666666;
+ --border-color: #e1e4e8;
+ --accent-color: #ec4899;
+ --code-bg: #f6f8fa;
+ --link-color: #ec4899;
+ --link-hover: #db2777;
+}
+
+@media (prefers-color-scheme: dark) {
+ :root {
+ --bg-primary: #0d1117;
+ --text-primary: #c9d1d9;
+ --text-secondary: #8b949e;
+ --border-color: #30363d;
+ --accent-color: #f472b6;
+ --code-bg: #161b22;
+ --link-color: #f472b6;
+ --link-hover: #f9a8d4;
+ }
+}
+
+* {
+ box-sizing: border-box;
+}
+
+body {
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans', Helvetica, Arial, sans-serif;
+ line-height: 1.6;
+ color: var(--text-primary);
+ background-color: var(--bg-primary);
+ margin: 0;
+ padding: 0;
+}
+
+h1 {
+ font-size: 2.5rem;
+ font-weight: 600;
+ margin: 0 0 2rem 0;
+ border-bottom: 1px solid var(--border-color);
+ padding-bottom: 0.5rem;
+}
+
+h2 {
+ font-size: 1.8rem;
+ font-weight: 600;
+ margin: 2rem 0 1rem 0;
+ padding-top: 1rem;
+}
+
+h4 {
+ font-size: 1.1rem;
+ font-weight: 600;
+ margin: 1.5rem 0 1rem 0;
+ font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
+}
+
+a {
+ color: var(--link-color);
+ text-decoration: none;
+}
+
+a:hover {
+ color: var(--link-hover);
+ text-decoration: underline;
+}
+
+h4 a {
+ color: var(--text-primary);
+}
+
+h4 a:hover {
+ color: var(--accent-color);
+}
+
+.procedure h4 a,
+.type h4 a,
+.constant h4 a {
+ color: var(--accent-color);
+}
+
+.procedure h4 a:hover,
+.type h4 a:hover,
+.constant h4 a:hover {
+ opacity: 0.8;
+}
+
+.procedure h4 a[href^="#"],
+.type h4 a[href^="#"],
+.constant h4 a[href^="#"] {
+ color: var(--text-secondary);
+ font-size: 0.9em;
+}
+
+.procedure h4 a[href^="#"]:hover,
+.type h4 a[href^="#"]:hover,
+.constant h4 a[href^="#"]:hover {
+ color: var(--text-primary);
+}
+
+.index {
+ margin: 2rem 0;
+}
+
+.index h2 {
+ margin: 0 0 1rem 0;
+ padding-top: 0;
+}
+
+.index h2 a {
+ color: var(--text-primary);
+}
+
+.index h3 {
+ font-size: 1.1rem;
+ font-weight: 600;
+ margin: 1rem 0 0.5rem 0;
+ display: inline;
+ cursor: pointer;
+}
+
+.index ul {
+ margin: 0.25rem 0 0.5rem 1rem;
+ padding: 0;
+ list-style: none;
+}
+
+.index li {
+ margin: 0;
+}
+
+.index a {
+ color: var(--link-color);
+ font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
+}
+
+.index a:hover {
+ color: var(--link-hover);
+}
+
+.all-constants,
+.all-procedures,
+.all-macros,
+.all-types {
+ margin: 2rem 0;
+}
+
+.constant,
+.procedure,
+.macro,
+.type {
+ margin: 1.5rem 0;
+}
+
+pre.code {
+ background-color: var(--code-bg);
+ border: 1px solid var(--border-color);
+ border-radius: 6px;
+ padding: 1rem;
+ margin: 1rem 0;
+ overflow-x: auto;
+ font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
+ font-size: 0.9rem;
+ line-height: 1.4;
+}
+
+span.ident {
+ color: var(--accent-color);
+}
+
+.example {
+ margin: 1rem 0;
+}
+
+.example pre.code {
+ margin: 0.5rem 0 0 0;
+}
+
+hr {
+ border: none;
+ height: 1px;
+ background-color: var(--border-color);
+ margin: 2rem 0;
+}
+
+details {
+ margin: 0.5rem 0;
+}
+
+summary {
+ cursor: pointer;
+ font-size: 1.1rem;
+ font-weight: 600;
+ margin: 1rem 0 0.5rem 0;
+}
+
+summary::-webkit-details-marker,
+summary::marker {
+ display: none;
+}
+
+.example summary {
+ font-size: 1rem;
+ font-weight: 500;
+ margin: 0.5rem 0;
+}
+
+.example summary p {
+ margin: 0;
+ display: inline;
+}
+
+.index details {
+ margin: 0.25rem 0;
+}
+
+.index summary {
+ margin: 0.5rem 0 0.25rem 0;
+}
+
+main {
+ max-width: 900px;
+ margin: 0 auto;
+ padding: 2rem;
+}
+
+@media (max-width: 768px) {
+ main {
+ padding: 1rem;
+ }
+
+ h1 {
+ font-size: 2rem;
+ }
+
+ h2 {
+ font-size: 1.6rem;
+ margin: 1.5rem 0 0.75rem 0;
+ }
+}
+
+a:focus,
+button:focus {
+ outline: 2px solid var(--accent-color);
+ outline-offset: 2px;
+}
+
+html {
+ scroll-behavior: smooth;
+}
+
+.back-to-index {
+ position: fixed;
+ bottom: 2rem;
+ right: 2rem;
+ background-color: var(--accent-color);
+ color: white;
+ border: none;
+ border-radius: 50%;
+ width: 3rem;
+ height: 3rem;
+ font-size: 1rem;
+ cursor: pointer;
+ z-index: 1000;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ text-decoration: none;
+ opacity: 0.5;
+ transition: all 0.3s ease;
+}
+
+.back-to-index:hover {
+ opacity: 1;
+ transform: translateY(-2px);
+ box-shadow: 0 6px 16px rgba(0, 0, 0, 0.2);
+ color: var(--bg-primary);
+ text-decoration: none;
+}
+
+.back-to-index:focus {
+ outline: 2px solid var(--accent-color);
+ outline-offset: 2px;
+}
+END;
+
+ResetCss :: #string END
+/* Box sizing rules */
+*,
+*::before,
+*::after {
+ box-sizing: border-box;
+}
+
+/* Prevent font size inflation */
+html {
+ -moz-text-size-adjust: none;
+ -webkit-text-size-adjust: none;
+ text-size-adjust: none;
+}
+
+/* Remove default margin in favour of better control in authored CSS */
+body, h1, h2, h3, h4, p,
+figure, blockquote, dl, dd {
+ margin-block-end: 0;
+}
+
+/* Remove list styles on ul, ol elements with a list role, which suggests default styling will be removed */
+ul[role='list'],
+ol[role='list'] {
+ list-style: none;
+}
+
+/* Set core body defaults */
+body {
+ min-height: 100vh;
+ line-height: 1.5;
+}
+
+/* Set shorter line heights on headings and interactive elements */
+h1, h2, h3, h4,
+button, input, label {
+ line-height: 1.1;
+}
+
+/* Balance text wrapping on headings */
+h1, h2,
+h3, h4 {
+ text-wrap: balance;
+}
+
+/* A elements that don't have a class get default styles */
+a:not([class]) {
+ text-decoration-skip-ink: auto;
+ color: currentColor;
+}
+
+/* Make images easier to work with */
+img,
+picture {
+ max-width: 100%;
+ display: block;
+}
+
+/* Inherit fonts for inputs and buttons */
+input, button,
+textarea, select {
+ font-family: inherit;
+ font-size: inherit;
+}
+
+/* Make sure textareas without a rows attribute are not tiny */
+textarea:not([rows]) {
+ min-height: 10em;
+}
+
+/* Anything that has been anchored to should have extra scroll margin */
+:target {
+ scroll-margin-block: 5ex;
+}
+
+END;
+
+#load "./module.jai";
+
+#import "String";
+#import "Compiler";
+
+using basic :: #import "Basic";
+#poke_name basic operator==;
+
+#import "File";
+#import "Hash_Table";
+#import "Sort";
diff --git a/_run_all_tests.jai b/_run_all_tests.jai
index 30acc14..4afde19 100644
--- a/_run_all_tests.jai
+++ b/_run_all_tests.jai
@@ -1,15 +1,8 @@
+RunTests :: true;
+
#scope_file #run {
compiler :: #import "Compiler";
compiler.set_build_options_dc(.{ do_output = false });
- #import "jc/array"(RUN_TESTS = true);
- #import "jc/encoding"(RUN_TESTS = true);
- #import "jc/hash"(RUN_TESTS = true);
- #import "jc/memory"(RUN_TESTS = true);
- #import "jc/meta"(RUN_TESTS = true);
- #import "jc/platform"(RUN_TESTS = true);
-
- rmath :: #import "jc/math"(.radians, RUN_TESTS = true);
- dmath :: #import "jc/math"(.degrees, RUN_TESTS = true);
- tmath :: #import "jc/math"(.turns, RUN_TESTS = true);
+ #load "module.jai";
}
diff --git a/ext/darwin/Foundation/NSObject.jai b/ext/darwin/Foundation/NSObject.jai
new file mode 100644
index 0000000..a811b4f
--- /dev/null
+++ b/ext/darwin/Foundation/NSObject.jai
@@ -0,0 +1 @@
+NSObject :: struct {};
diff --git a/ext/darwin/Foundation/NSString.jai b/ext/darwin/Foundation/NSString.jai
new file mode 100644
index 0000000..aaf294e
--- /dev/null
+++ b/ext/darwin/Foundation/NSString.jai
@@ -0,0 +1,21 @@
+NSString :: struct {
+ // #as super: NSObject;
+}
+
+init :: (path: ) -> NSString {
+}
+
+
+sel: struct {
+ init_withBytes_length_encoding: objc.Sel;
+ init_contentsOfFile_encoding: objc.Sel;
+ UTF8String: objc.Sel;
+};
+
+init_everything :: () {
+ sel.init_withBytes_length_encoding = objc.sel_register_name("init:withBytes:length:encoding");
+ sel.init_contentsOfFile_encoding = objc.sel_register_name("init:contentsOfFile:encoding");
+ sel.UTF8String = objc.sel_register_name("UTF8String");
+}
+
+// #import "jc/meta/init"(init_everything);
diff --git a/ext/darwin/Foundation/module.jai b/ext/darwin/Foundation/module.jai
new file mode 100644
index 0000000..868ef67
--- /dev/null
+++ b/ext/darwin/Foundation/module.jai
@@ -0,0 +1,8 @@
+NSObject :: struct {
+ id: u64;
+}
+
+NSString :: struct {
+ #as using isa: NSObject;
+}
+
diff --git a/ext/darwin/foundation.jai b/ext/darwin/foundation.jai
new file mode 100644
index 0000000..f8a13c5
--- /dev/null
+++ b/ext/darwin/foundation.jai
@@ -0,0 +1,38 @@
+
+NSNumber :: struct {
+ // #as using isa: NSObject;
+}
+
+#scope_file;
+
+Sel :: uptr;
+
+sel: struct {
+ boolValue: Sel; @boolValue
+ intValue: Sel; @intValue
+ unsignedIntValue: Sel; @unsignedIntValue
+ floatValue: Sel; @floatValue
+ doubleValue: Sel; @doubleValue
+}
+
+#add_context InitFoundation :: () {
+ #import "Basic";
+
+ tmp: [512]u8;
+
+ base := (*sel).(*u8);
+ info := type_info(type_of(sel));
+ for info.members if it.notes.count != 0 {
+ name := it.notes[0];
+ memcpy(tmp.data, name.data, name.count);
+ tmp.data[name.count] = 0;
+
+ ptr := base + it.offset_in_bytes;
+ ptr.* = sel_register_name(tmp.data);
+ assert(ptr.* != 0, "failed to register selector: %", name);
+ }
+
+ print("%\n", info.*);
+}
+
+Foundation :: #library,system,no_dll,link_always "Foundation";
diff --git a/ext/darwin/module.jai b/ext/darwin/module.jai
new file mode 100644
index 0000000..5788182
--- /dev/null
+++ b/ext/darwin/module.jai
@@ -0,0 +1,47 @@
+#scope_export;
+
+UInt :: u64;
+
+Id :: *object;
+Class :: *class;
+Sel :: *selector;
+
+Bool :: u8;
+
+True : Bool : 1;
+False : Bool : 0;
+
+msg_send :: () #foreign objc "objc_msgSend";
+msg_send_super :: () #foreign objc "objc_msgSend_super";
+msg_send_fpret :: () #foreign objc "objc_msgSend_fpret";
+msg_send_stret :: () #foreign objc "objc_msgSend_stret";
+
+get_class :: (name: *u8) -> Class #foreign objc "objc_getClass";
+
+sel_get_name :: (sel: Sel) -> *u8 #foreign objc "sel_getName";
+sel_register_name :: (str: *u8) -> Sel #foreign objc "sel_registerName";
+sel_get_uid :: (str: *u8) -> Sel #foreign objc "sel_getUid";
+
+obj_get_class :: (obj: Id) -> Class #foreign objc "object_getClass";
+obj_set_class :: (obj: Id, cls: Class) -> Class #foreign objc "object_setClass";
+obj_is_class :: (obj: Id) -> Bool #foreign objc "object_isClass";
+obj_get_class_name :: (obj: Id) -> *u8 #foreign objc "object_getClassName";
+obj_copy :: (obj: Id, size: u64) -> Id #foreign objc "object_copy";
+obj_dispose :: (obj: Id) -> Id #foreign objc "object_dispose";
+
+class_get_name :: (cls: Class) -> *u8 #foreign objc "class_getName";
+class_get_super :: (cls: Class) -> Class #foreign objc "class_getSuperclass";
+
+#scope_module;
+
+class :: struct {};
+object :: struct {};
+method :: struct {};
+ivar :: struct {};
+category :: struct {};
+protocol :: struct {};
+selector :: struct {};
+
+objc :: #library,system,link_always,no_dll "libobjc";
+
+#import "jc";
diff --git a/ext/hmm/README b/ext/hmm/README
deleted file mode 100644
index deffa4d..0000000
--- a/ext/hmm/README
+++ /dev/null
@@ -1,46 +0,0 @@
--------------
-Handmade Math
--------------
-
- jai ./generate.jai # generate the bindings (not required)
-
- #import "jc/hmm"(
- STATIC = true, # if HMM should be linked statically (default: true)
- SIMD = true, # if SIMD should be used (default: true)
- UNITS = .radians, # angle units to use [radians, degrees, turns] (default: radians)
- );
-
-What
-----
-
-Configurable, auto-generated bindings for Handmade Math
-
-How
----
-
-These are generated from HandmadeMath.h using Jai's
-Bindings_Generator module. Because HandmadeMath is a
-header-only library, we need to compile it into a
-static/dynamic library that can be used with Jai's FFI
-system. 'generate.jai' creates both static and dynamic
-libraries for each angle unit in HandmadeMath
-(radians, degrees, turns) +- SIMD support. These are
-placed in the corresponding 'win', 'mac', or 'linux'
-directories.
-
-'module.jai' conditionally links one of these libraries
- based on the module parameters set.
-
-A few liberties were taken during the binding process to
-either fix issues with automatic binding generation, or
-improve the usability of these bindings.
-
-Here are the main changes:
-
- - Converted procedure argument names from PascalCase
- to snake_case
-
- - Converted struct field names from PascalCase to
- snake_case
-
- - Procedure names still use PascalCase
diff --git a/ext/hmm/module.jai b/ext/hmm/module.jai
index 0f9d9eb..ac96029 100644
--- a/ext/hmm/module.jai
+++ b/ext/hmm/module.jai
@@ -1,7 +1,29 @@
+/*
+ Module hmm provides bindings for the HandmadeMath C
+ library.
+
+ hmm conditionally links to a specific version of
+ HandmadeMath based on the module parameters passed
+ when importing.
+
+ Additionally, a few liberties were taken during the
+ binding process to either fix issues with automatic
+ binding generation, or improve the usability of these
+ bindings.
+
+ Here are the main changes:
+
+ - Converted procedure argument names from PascalCase to snake_case
+ - Converted struct field names from PascalCase to snake_case
+
+*/
#module_parameters(
+ /// Statically link to HandmadeMath.
STATIC := true,
- SIMD := true,
- UNITS := (enum { radians; degrees; turns; }).radians
+ /// Enable SIMD support.
+ SIMD := true,
+ /// Angle units to use.
+ UNITS : enum { radians; degrees; turns; } = .radians
);
#scope_export;
diff --git a/ext/luajit/module.jai b/ext/luajit/module.jai
index 6dd9b9e..cd7d641 100644
--- a/ext/luajit/module.jai
+++ b/ext/luajit/module.jai
@@ -1,3 +1,7 @@
+/*
+ Module luajit provides bindings for the LuaJIT C
+ library (2.1.1744318430)
+*/
#module_parameters(STATIC := true);
#if STATIC {
diff --git a/ext/objc/module.jai b/ext/objc/module.jai
new file mode 100644
index 0000000..5788182
--- /dev/null
+++ b/ext/objc/module.jai
@@ -0,0 +1,47 @@
+#scope_export;
+
+UInt :: u64;
+
+Id :: *object;
+Class :: *class;
+Sel :: *selector;
+
+Bool :: u8;
+
+True : Bool : 1;
+False : Bool : 0;
+
+msg_send :: () #foreign objc "objc_msgSend";
+msg_send_super :: () #foreign objc "objc_msgSend_super";
+msg_send_fpret :: () #foreign objc "objc_msgSend_fpret";
+msg_send_stret :: () #foreign objc "objc_msgSend_stret";
+
+get_class :: (name: *u8) -> Class #foreign objc "objc_getClass";
+
+sel_get_name :: (sel: Sel) -> *u8 #foreign objc "sel_getName";
+sel_register_name :: (str: *u8) -> Sel #foreign objc "sel_registerName";
+sel_get_uid :: (str: *u8) -> Sel #foreign objc "sel_getUid";
+
+obj_get_class :: (obj: Id) -> Class #foreign objc "object_getClass";
+obj_set_class :: (obj: Id, cls: Class) -> Class #foreign objc "object_setClass";
+obj_is_class :: (obj: Id) -> Bool #foreign objc "object_isClass";
+obj_get_class_name :: (obj: Id) -> *u8 #foreign objc "object_getClassName";
+obj_copy :: (obj: Id, size: u64) -> Id #foreign objc "object_copy";
+obj_dispose :: (obj: Id) -> Id #foreign objc "object_dispose";
+
+class_get_name :: (cls: Class) -> *u8 #foreign objc "class_getName";
+class_get_super :: (cls: Class) -> Class #foreign objc "class_getSuperclass";
+
+#scope_module;
+
+class :: struct {};
+object :: struct {};
+method :: struct {};
+ivar :: struct {};
+category :: struct {};
+protocol :: struct {};
+selector :: struct {};
+
+objc :: #library,system,link_always,no_dll "libobjc";
+
+#import "jc";
diff --git a/ext/raylib/module.jai b/ext/raylib/module.jai
index b080be5..503dc97 100644
--- a/ext/raylib/module.jai
+++ b/ext/raylib/module.jai
@@ -1,3 +1,10 @@
+/*
+ Module raylib provides bindings for the raylib C
+ library (v5.5).
+
+ Supported platforms: Windows, Mac, Linux
+*/
+
#module_parameters(STATIC := true);
#scope_export
diff --git a/ext/remotery/module.jai b/ext/remotery/module.jai
index c956ee4..23ec8f2 100644
--- a/ext/remotery/module.jai
+++ b/ext/remotery/module.jai
@@ -1,3 +1,7 @@
+/*
+ Module remotery provides bindings for the Remotery
+ CPU/GPU profiling library.
+*/
#module_parameters(STATIC := true);
#if STATIC {
diff --git a/internal/array.jai b/internal/array.jai
new file mode 100644
index 0000000..7039bc4
--- /dev/null
+++ b/internal/array.jai
@@ -0,0 +1,205 @@
+/// Slice returns a subsection of an array.
+Slice :: (view: []$T, start_idx: int, count := -1, loc := #caller_location) -> []T {
+ AssertCallsite(start_idx >= +0 && start_idx < view.count, "incorrect slice bounds");
+ AssertCallsite(count >= -1 && count < view.count, "incorrect slice length");
+
+ if count == -1
+ { count = view.count - start_idx; }
+
+ return .{ data = view.data + start_idx, count = count };
+}
+
+/// Reset sets an array's length to 0, allowing it to be reused
+/// without allocating new memory.
+Reset :: (view: *[]$T) {
+ view.count = 0;
+}
+
+/// Clear zeroes the memory of an array and sets its length to 0.
+///
+/// Note: Clear does not free the array's memory.
+Clear :: (view: *[]$T) {
+ MemZero(view.data, view.count * size_of(T));
+ view.count = 0;
+}
+
+/// Equal checks the equality of two arrays.
+Equal :: (lhs: []$T, rhs: []T) -> bool {
+ if lhs.count != rhs.count
+ { return false; }
+ return MemEqual(lhs.data, rhs.data, lhs.count * size_of(T));
+}
+
+
+FindFlags :: enum_flags {
+ Last; // The last matching element should be returned.
+ FromEnd; // The search be done in reverse.
+}
+
+FindResult :: struct(T: Type) {
+ value: T = ---;
+ index: int;
+}
+
+/// Find searches through an array, returning the first element
+/// that matches the given value.
+Find :: (view: []$T, value: T, $flags: FindFlags = 0) -> (bool, FindResult(T)) {
+ found: bool;
+
+ result: FindResult(T);
+ result.index = -1;
+
+ REVERSE :: #run (flags & .FromEnd).(bool);
+ for #v2 <=REVERSE view if it == value {
+ found = true;
+ result.index = it_index;
+ result.value = it;
+ #if !(flags & .Last) break;
+ }
+
+ return found, result;
+}
+
+/// Contains checks if the given value exists in an array.
+Contains :: (view: []$T, value: T) -> bool {
+ return Find(view, value);
+}
+
+
+TrimFlags :: enum_flags {
+ FromStart; // The start of the array should be trimmed.
+ FromEnd; // The end of the array should be trimmed.
+ MatchInFull; // Only trim when the cutset matches exactly.
+}
+
+/// Trim returns a subsection of an array with all leading/trailing values
+/// from cutset removed.
+Trim :: (view: []$T, cutset: []T, $flags: TrimFlags = .FromStart) -> []T {
+ result := view;
+ if cutset.count == 0 || cutset.count > view.count {
+ return result;
+ }
+
+ #if flags & .FromStart {
+ #if flags & .MatchInFull {
+ if Equal(Slice(view, 0, cutset.count), cutset) {
+ result = Slice(view, cutset.count, -1);
+ }
+ }
+ else {
+ while result.count > 0 {
+ if !Contains(cutset, result[0]) {
+ break;
+ }
+
+ result.data += 1;
+ result.count -= 1;
+ }
+ }
+ }
+
+ #if flags & .FromEnd {
+ #if flags & .MatchInFull {
+ if Equal(Slice(view, view.count - cutset.count), cutset) {
+ result.count -= cutset.count;
+ }
+ }
+ else {
+ while result.count > 0 {
+ if !Contains(cutset, result[result.count - 1]) {
+ break;
+ }
+
+ result.count -= 1;
+ }
+ }
+ }
+
+ return result;
+}
+
+
+#scope_file
+
+#if #exists(RunTests) #run,stallable {
+ Test("slice", t => {
+ a1 := int.[ 1, 2, 3, 4, 5 ];
+ a2 := Slice(a1, 2);
+ Expect(a2.count == 3);
+ Expect(Equal(a2, int.[ 3, 4, 5 ]));
+
+ b1 := int.[ 1, 2, 3, 4, 5 ];
+ b2 := Slice(b1, 2, 0);
+ Expect(b2.count == 0);
+ Expect(b2.data == b1.data + 2);
+
+ c1 := int.[ 1, 2, 3, 4, 5 ];
+ c2 := Slice(c1, 3, 1);
+ Expect(c2.count == 1);
+ Expect(Equal(c2, int.[ 4 ]));
+
+ d1 := int.[ 1, 2, 3 ];
+ d2 := Slice(d1, 2);
+ Expect(d2.count == 1);
+ Expect(Equal(d2, int.[ 3 ]));
+ });
+
+ Test("find", t => {
+ a := int.[ 1, 2, 3, 4, 5 ];
+
+ ok, res := Find(a, 3);
+ Expect(ok && res.index == 2);
+ Expect(res.value == 3);
+
+ ok, res = Find(a, -1);
+ Expect(!ok && res.index == -1);
+
+ b := int.[ 1, 2, 2, 3, 4, 5, 2 ];
+
+ ok, res = Find(b, 2);
+ Expect(ok && res.index == 1);
+
+ ok, res = Find(b, 2, .FromEnd);
+ Expect(ok && res.index == 6);
+
+ c := int.[ 0, 0, 0, 1, 2, 3, 0, 0, 0 ];
+
+ ok, res = Find(c, 0, .Last);
+ Expect(ok && res.index == 8);
+
+ ok, res = Find(c, 0, .FromEnd | .Last);
+ Expect(ok && res.index == 0);
+ });
+
+ Test("contains", t => {
+ a := int.[ 1, 2, 3, 4, 5 ];
+ Expect(Contains(a, 3));
+ Expect(!Contains(a, -1));
+ });
+
+ Test("trim", t => {
+ a1 := int.[ 0, 0, 0, 1, 2, 3 ];
+ a2 := Trim(a1, .[ 0 ]);
+ Expect(Equal(a1, .[ 0, 0, 0, 1, 2, 3 ]));
+ Expect(Equal(a2, .[ 1, 2, 3 ]));
+
+ b1 := int.[ 0, 0, 0, 1, 2, 3, 0, 0, 0 ];
+ b2 := Trim(b1, .[ 0 ], .FromEnd);
+ Expect(Equal(b1, .[ 0, 0, 0, 1, 2, 3, 0, 0, 0 ]));
+ Expect(Equal(b2, .[ 0, 0, 0, 1, 2, 3 ]));
+
+ c1 := int.[ 0, 0, 0, 1, 2, 3, 0, 0, 0 ];
+ c2 := Trim(c1, .[ 0 ], .FromStart | .FromEnd);
+ Expect(Equal(c1, .[ 0, 0, 0, 1, 2, 3, 0, 0, 0 ]));
+ Expect(Equal(c2, .[ 1, 2, 3 ]));
+
+ d1 := int.[ 0, 0, 0, 1, 2, 3, 0, 0, 0 ];
+ d2 := Trim(d1, .[ 0, 0, 0 ], .FromStart | .MatchInFull);
+ d3 := Trim(d1, .[ 0, 0, 0 ], .FromEnd | .MatchInFull);
+ d4 := Trim(d1, .[ 0, 0, 0 ], .FromStart | .FromEnd | .MatchInFull);
+ Expect(Equal(d1, .[ 0, 0, 0, 1, 2, 3, 0, 0, 0 ]));
+ Expect(Equal(d2, .[ 1, 2, 3, 0, 0, 0 ]));
+ Expect(Equal(d3, .[ 0, 0, 0, 1, 2, 3 ]));
+ Expect(Equal(d4, .[ 1, 2, 3 ]));
+ });
+}
diff --git a/internal/builtin.jai b/internal/builtin.jai
new file mode 100644
index 0000000..3ceb238
--- /dev/null
+++ b/internal/builtin.jai
@@ -0,0 +1,180 @@
+/// JcMajor is the current major version of this library.
+///
+/// Major versions are guaranteed to have stable apis
+/// for their duration.
+JcMajor :: 0;
+
+/// JcMinor is the current minor version of this library.
+///
+/// Minor versions denote bug fixes, additions, or improvements
+/// that do not affect api stability.
+JcMinor :: 1;
+
+// @note(judah): we can't use range_of here because a compiler bug?
+
+S8Min :: #run min_of(s8); /// -128 (-0x80)
+S8Max :: #run max_of(s8); /// 127 (0x7f)
+S16Min :: #run min_of(s16); /// -32768 (-0x8000)
+S16Max :: #run max_of(s16); /// 32767 (0x7f_ff)
+S32Min :: #run min_of(s32); /// -2147483648 (-0x8000_0000)
+S32Max :: #run max_of(s32); /// 2147483647 (0x7fff_ffff)
+S64Min :: #run min_of(s64); /// -9223372036854775808 (-0x80000000_00000000)
+S64Max :: #run max_of(s64); /// 9223372036854775807 (0x7fffffff_ffffffff)
+
+U8Min :: #run min_of(u8); /// 0 (0x00)
+U8Max :: #run max_of(u8); /// 255 (0xff)
+U16Min :: #run min_of(u16); /// 0 (0x00_00)
+U16Max :: #run max_of(u16); /// 65535 (0xff_ff)
+U32Min :: #run min_of(u32); /// 0 (0x0000_0000)
+U32Max :: #run max_of(u32); /// 4294967295 (0xffff_ffff)
+U64Min :: #run min_of(u64); /// 0 (0x00000000_00000000)
+U64Max :: #run max_of(u64); /// 18446744073709551615 (0xffffffff_ffffffff)
+
+Float32Min :: #run min_of(float32); /// 1.17549e-38 (0h0080_0000)
+Float32Max :: #run max_of(float32); /// 3.40282e+38 (0h7f7fffff)
+Float64Min :: #run min_of(float64); /// 2.22507e-308 (0h00100000_00000000)
+Float64Max :: #run max_of(float64); /// 1.79769e+308 (0h7fefffff_ffffffff)
+
+/// m0 is a 0-size marker type.
+///
+/// It allows specific offsets within a type to be marked which is useful for (de)serialization.
+///
+/// MyType :: struct {
+/// do_not_serialize_1: *void;
+///
+/// _start: m0; // Has the same offset as serialize_1
+/// serialize_1: [32]u8;
+/// serialize_2: u64;
+/// serialize_3: bool;
+/// serialize_4: float32;
+/// _end: m0; // Has the same offset as serialize_4
+///
+/// do_not_serialize_2: [..]int;
+/// }
+///
+/// value := MyType.{};
+/// start := *value + offset_of(value, #code _start);
+/// end := *value + offset_of(value, #code _end);
+/// WriteToDisk(data = start, count = end - start);
+///
+m0 :: #type void;
+
+b8 :: enum u8 { false_ :: (0 != 0).(u8); true_ :: (0 == 0).(u8); }; /// b8 is an 8-bit boolean.
+b16 :: enum u16 { false_ :: (0 != 0).(u16); true_ :: (0 == 0).(u16); }; /// b16 is a 16-bit boolean.
+b32 :: enum u32 { false_ :: (0 != 0).(u32); true_ :: (0 == 0).(u32); }; /// b32 is a 32-bit boolean.
+b64 :: enum u64 { false_ :: (0 != 0).(u64); true_ :: (0 == 0).(u64); }; /// b64 is a 64-bit boolean.
+
+/// Panic displays the given message and crashes the program.
+///
+/// Note: Defers will not run when Panic is called.
+Panic :: (message := "runtime panic", loc := #caller_location) #expand #no_debug {
+ #if DebugBuild {
+ WriteStderrLocation(loc);
+ WriteStderrString(": ");
+ }
+
+ WriteStderrString(message, "\n");
+ Trap();
+}
+
+/// Unreachable displays the given message and causes an execution trap.
+///
+/// Note: Defers will not run when Unreachable is called.
+Unreachable :: (message := "unreachable code hit", loc := #caller_location) #expand #no_debug {
+ trap :: #ifx DebugBuild then DebugTrap else Trap;
+
+ #if DebugBuild {
+ WriteStderrLocation(loc);
+ WriteStderrString(": ");
+ }
+
+ WriteStderrString(message, "\n");
+ trap();
+}
+
+/// CompileError displays the given message and stops compilation.
+///
+/// Note: By default, the error is reported at the callsite.
+CompileError :: (message: string, loc := #caller_location) #expand #no_debug #compile_time {
+ if #compile_time {
+ compiler_report(message, loc, .ERROR);
+ }
+ else {
+ Panic("CompileError can only be called at compile-time", loc = loc);
+ }
+}
+
+// @todo(judah): these should be different!
+
+/// Trap causes an execution trap.
+Trap :: () #expand #no_debug {
+ debug_break(); // Provided by Runtime_Support
+}
+
+/// DebugTrap causes an execution trap that grabs the attention of a debugger.
+DebugTrap :: () #expand #no_debug {
+ debug_break(); // Provided by Runtime_Support
+}
+
+#if DebugBuild
+{
+ /// Assert causes a debug break if the given condition is false.
+ Assert :: (cond: bool, message := "condition was false", loc := #caller_location) #expand #no_debug {
+ // @note(judah): We only need to do this to route into the context's builtin assertion handling.
+ if cond || context.handling_assertion_failure return;
+
+ context.handling_assertion_failure = true;
+ should_trap := context.assertion_failed(loc, message);
+ context.handling_assertion_failure = false;
+
+ if should_trap {
+ DebugTrap();
+ }
+ }
+
+ /// AssertCallsite works identically to Assert, except that it expects a
+ /// Source_Code_Location (called 'loc') to exist in the calling scope.
+ ///
+ /// MyProc :: (loc := #caller_location) {
+ /// AssertCallsite(false); // 'loc' is passed implicitly
+ /// Assert(false, loc = loc); // equivalent
+ /// }
+ ///
+ AssertCallsite :: (cond: bool, message := "condition was false") #expand #no_debug {
+ Assert(cond, message, loc = `loc);
+ }
+}
+else
+{
+ // @note(judah): these need to be separate declarations so we can use #discard.
+ // otherwise, the compiler will generate instructions to setup the call when assertions are disabled.
+ Assert :: (#discard cond: bool, #discard message := "", #discard loc := #caller_location) #expand #no_debug {}
+ AssertCallsite :: (#discard cond: bool, #discard message := "") #expand #no_debug {}
+}
+
+
+#scope_module
+
+WriteString :: write_strings; @jc.nodocs // Provided by Runtime_Support
+WriteNumber :: write_number; @jc.nodocs // Provided by Runtime_Support
+
+// @note(judah): This is a direct copy of Runtime_Support's write_loc since it's not exported
+WriteStderrLocation :: (loc: Source_Code_Location) {
+ WriteStderrString(loc.fully_pathed_filename, ":");
+ WriteStderrNumber(loc.line_number);
+ WriteStderrString(",");
+ WriteStderrNumber(loc.character_number);
+} @jc.nodocs
+
+#scope_file
+
+DebugBuild :: #run -> bool {
+ // @note(judah): there's not really a good way to detect opt level/build type,
+ // so just check if debug info is being emitted.
+ #import "Compiler";
+ opts := get_build_options();
+ return opts.emit_debug_info != .NONE;
+};
+
+WriteStderrString :: #bake_arguments write_strings(to_standard_error = true); // Provided by Runtime_Support
+WriteStderrNumber :: #bake_arguments write_number(to_standard_error = true); // Provided by Runtime_Support
diff --git a/internal/keywords.jai b/internal/keywords.jai
new file mode 100644
index 0000000..2fb7110
--- /dev/null
+++ b/internal/keywords.jai
@@ -0,0 +1,233 @@
+/// offset_of returns the byte offset of a field within the type T.
+///
+/// Note: T must be a struct type.
+///
+/// MyType :: struct { x: int; y: int; z: int; };
+/// offset_of(MyType, #code y); // 8
+///
+offset_of :: ($T: Type, ident: Code, loc := #caller_location) -> int #expand {
+ #run (loc: Source_Code_Location) {
+ info := type_info(T);
+ if info.type != .STRUCT {
+ CompileError("offset_of can only be used on struct types", loc = loc);
+ }
+ }(loc);
+
+ return #run -> int {
+ t: T = ---;
+ return (*t.#insert ident).(*void) - (*t).(*void);
+ };
+}
+
+/// offset_of returns the byte offset of a field within the type of value.
+///
+/// Note: If offset_of is given a pointer value, it will use the type pointed to.
+///
+/// value := struct{ x: int; y: int; z: int; }.{};
+/// offset_of(value, #code y); // 8
+///
+offset_of :: (#discard value: $T, ident: Code, loc := #caller_location) -> int #expand {
+ type :: #run -> Type {
+ info := T.(*Type_Info);
+ if info.type == .POINTER {
+ info = info.(*Type_Info_Pointer).pointer_to;
+ // @question(judah): do we want it to traverse all the way up to a non-pointer type?
+ // I opted against because if you have a *T, you only want offset_of to get an offset
+ // from that pointer. What would you do with a field offset from **T?
+ if info.type == .POINTER {
+ CompileError("offset_of only allows one level of pointer indirection.", loc = loc);
+ }
+ }
+
+ return get_type(info);
+ };
+
+ return offset_of(type, ident, loc = loc);
+}
+
+/// align_of returns the alignment of the given type.
+align_of :: ($T: Type) -> int #expand {
+ return #run -> int {
+ if size_of(T) == 0
+ { return 0; }
+
+ info := type_info(struct{ p: u8; t: T; });
+ return info.members[1].offset_in_bytes.(int);
+ };
+}
+
+/// default_of returns a value of type T as if it was just instantiated.
+///
+/// Note: default_of will call the initializer for aggregate types, so you
+/// may want zero_of instead.
+default_of :: ($T: Type) -> T #expand {
+ default: T;
+ return default;
+}
+
+/// undefined_of returns a value of type T that has not been initialized.
+undefined_of :: ($T: Type) -> T #expand {
+ uninit: T = ---;
+ return uninit;
+}
+
+/// zero_of returns a value of type T that has been zero-initialized.
+///
+/// Note: zero_of will not call the initializer for aggregate types, so you
+/// may want default_of instead.
+zero_of :: ($T: Type) -> T #expand {
+ zero := undefined_of(T);
+ MemZero(*zero);
+ return zero;
+}
+
+/// min_of returns the minimum value T can represent.
+///
+/// Note: T must be an integer, float, or enum type.
+min_of :: ($T: Type, loc := #caller_location) -> T #expand {
+ return #run -> T {
+ info := T.(*Type_Info);
+ if info.type == {
+ case .INTEGER;
+ i := info.(*Type_Info_Integer);
+ if i.runtime_size == {
+ case 1; return (ifx i.signed then -0x80 else 0).(T, no_check);
+ case 2; return (ifx i.signed then -0x8000 else 0).(T, no_check);
+ case 4; return (ifx i.signed then -0x8000_0000 else 0).(T, no_check);
+ case 8; return (ifx i.signed then -0x8000_0000_0000_0000 else 0).(T, no_check);
+ case ; CompileError("unknown integer size", loc = loc);
+ }
+
+ case .FLOAT;
+ if info.runtime_size == {
+ case 4; return (0h0080_0000).(T, no_check);
+ case 8; return (0h00100000_00000000).(T, no_check);
+ case ; CompileError("unknown float size", loc = loc);
+ }
+
+ case .ENUM;
+ i := info.(*Type_Info_Enum);
+ if i.values.count == 0 {
+ return 0;
+ }
+
+ min: T = i.values[0].(T, no_check);
+ if i.internal_type.signed {
+ for i.values if it.(T) < min {
+ min = it.(T);
+ }
+ }
+ else {
+ for i.values if it.(T) < min {
+ min = it.(T);
+ }
+ }
+
+ return min;
+
+ case;
+ CompileError("min_of requires an enum, integer, or float type", loc = loc);
+ }
+
+ return 0;
+ };
+}
+
+/// max_of returns the maximum value T can represent.
+///
+/// Note: T must be an integer, float, or enum type.
+max_of :: ($T: Type, loc := #caller_location) -> T #expand {
+ return #run -> T {
+ info := T.(*Type_Info);
+ if info.type == {
+ case .INTEGER;
+ i := info.(*Type_Info_Integer);
+ if i.runtime_size == {
+ case 1; return (ifx i.signed then 0x7f else 0xff).(T, no_check);
+ case 2; return (ifx i.signed then 0x7fff else 0xffff).(T, no_check);
+ case 4; return (ifx i.signed then 0x7fff_ffff else 0xffff_ffff).(T, no_check);
+ case 8; return (ifx i.signed then 0x7fff_ffff_ffff_ffff else 0xffff_ffff_ffff_ffff).(T, no_check);
+ case ; CompileError("unknown integer size", loc = loc);
+ }
+
+ case .FLOAT;
+ if info.runtime_size == {
+ case 4; return (0h7F7FFFFF).(T, no_check);
+ case 8; return (0h7FEFFFFF_FFFFFFFF).(T, no_check);
+ case ; CompileError("unknown float size", loc = loc);
+ }
+
+ case .ENUM;
+ i := info.(*Type_Info_Enum);
+ if i.values.count == 0 {
+ return 0;
+ }
+
+ max := i.values[0].(T, no_check);
+ if i.internal_type.signed {
+ for i.values if xx it > max {
+ max = xx it;
+ }
+ }
+ else {
+ for i.values if xx it > max {
+ max = xx it;
+ }
+ }
+
+ return max;
+
+ case;
+ CompileError("max_of requires an enum, integer, or float type", loc = loc);
+ }
+
+ return 0;
+ };
+}
+
+/// range_of returns the minimum and maximum values T can represent.
+///
+/// Note: T must be an integer, float, or enum type.
+range_of :: ($T: Type, loc := #caller_location) -> (T, T) #expand {
+ return min_of(T, loc = loc), max_of(T, loc = loc);
+}
+
+/// sector creates a named block that can exit early via the 'break' keyword.
+///
+/// Note: The block created by sector is called 'early' by default.
+///
+/// for sector() {
+/// break;
+/// break early; // automatically created
+/// }
+///
+/// for sector("render_player") {
+/// break render_player;
+/// }
+///
+sector :: ($name := Sector().Name) -> Sector(name) #expand { return .{}; }
+
+// @note(judah): there seems to be a weird race condition in the compiler
+// that causes this to hit a null reference check error if running at compile-time.
+for_expansion :: (v: Sector, code: Code, _: For_Flags) #expand {
+ // @todo(judah): fix this case?
+ // 'for this: sector() { break early; break this; }'
+ // both names valid here!
+ #insert #run basic.tprint(#string END
+ for `%1: 0..0 {
+ `it :: #run zero_of(void);
+ `it_index :: #run zero_of(void);
+ #insert,scope(code) code;
+ }
+ END,
+ // @note(judah): guards against calling this_block with
+ // an empty string which results in weird error messages.
+ ifx v.Name.count != 0 v.Name else Sector().Name);
+}
+
+
+#scope_file
+
+Sector :: struct(Name: string = "early") {}
+
+basic :: #import "Basic";
diff --git a/internal/memory.jai b/internal/memory.jai
new file mode 100644
index 0000000..aa49082
--- /dev/null
+++ b/internal/memory.jai
@@ -0,0 +1,55 @@
+/// MemEqual checks the equality of two pieces of memory.
+///
+/// Note: MemEqual will panic if size_in_bytes is negative.
+MemEqual :: (p1: *void, p2: *void, size_in_bytes: int) -> bool {
+ if size_in_bytes < 0
+ { Panic("size_in_bytes cannot be negative"); }
+ return memcmp(p1, p2, size_in_bytes) == 0; // Provided by Preload
+}
+
+/// MemCopy copies the memory of src to dst.
+///
+/// Note: MemCopy will panic if size_in_bytes is negative.
+MemCopy :: (dst: *void, src: *void, size_in_bytes: int) {
+ if size_in_bytes < 0
+ { Panic("size_in_bytes cannot be negative"); }
+ memcpy(dst, src, size_in_bytes); // Provided by Preload
+}
+
+/// MemOverwrite overwites the memory of p with value.
+///
+/// Note: MemOverwrite will panic if size_in_bytes is negative.
+MemOverwrite :: (p: *void, size_in_bytes: int, value: u8 = 0) {
+ if size_in_bytes < 0
+ { Panic("size_in_bytes cannot be negative"); }
+ memset(p, value, size_in_bytes); // Provided by preload
+}
+
+/// MemZero zeroes the memory of p.
+///
+/// Note: MemZero will panic if size_in_bytes is negative.
+MemZero :: (p: *void, size_in_bytes: int) {
+ MemOverwrite(p, size_in_bytes, 0);
+}
+
+/// MemZero zeroes the memory of p.
+///
+/// Note: MemZero will not call the initializer for aggregate types,
+/// so you may want MemReset instead.
+MemZero :: (p: *$T) {
+ MemOverwrite(p, size_of(T), 0);
+}
+
+/// MemReset resets the memory of p, as if it was just instantiated.
+///
+/// Note: MemReset will call the initializer for aggregate types, so you
+/// may want MemZero instead.
+MemReset :: (p: *$T) {
+ initializer :: initializer_of(T);
+ #if initializer {
+ inline initializer(p);
+ }
+ else {
+ inline MemZero(p);
+ }
+}
diff --git a/internal/module.jai b/internal/module.jai
new file mode 100644
index 0000000..b1d82d4
--- /dev/null
+++ b/internal/module.jai
@@ -0,0 +1 @@
+#assert false "This module (jc/internal) is not expected to be imported directly. Import 'jc' instead.";
diff --git a/internal/testing.jai b/internal/testing.jai
new file mode 100644
index 0000000..ce334ba
--- /dev/null
+++ b/internal/testing.jai
@@ -0,0 +1,115 @@
+// Usage:
+#if 0 {
+ #run,stallable {
+ Test("thing", t => {
+ Expect(some_condition, "error message: %", value);
+ });
+
+ Test("other thing", t => {
+ Expect(other_condition, "error message: %", value);
+ });
+
+ // ...
+ }
+}
+
+/// Test defines a new suite to be executed by the test runner.
+///
+/// See: Expect for more information.
+///
+/// Test("my_proc does what it should", t => {
+/// value1 := my_proc(/* ... */);
+/// Expect(value1 != 0, "my_proc returned zero!");
+///
+/// value2 := my_proc(/* .... */);
+/// Expect(value2 > 0, "my_proc returned a negative number!");
+/// });
+///
+Test :: (name: string, proc: (*void) -> (), loc := #caller_location) {
+ // @note(judah): incredibly dumb way to get nicer test runs
+ path := loc.fully_pathed_filename;
+
+ i := path.count - 1;
+ found_first_slash := false;
+
+ while i >= 0 {
+ if path[i] == "/" {
+ if found_first_slash {
+ i += 1;
+ break;
+ }
+
+ found_first_slash = true;
+ }
+
+ i -= 1;
+ }
+
+ if !found_first_slash {
+ path = strings.path_filename(loc.fully_pathed_filename);
+ }
+ else {
+ path.count -= i;
+ path.data += i;
+ }
+
+ WriteString(path, ",");
+ WriteNumber(loc.line_number);
+ WriteString(": ", name, "... ");
+
+ t: TestRun;
+ proc(*t);
+
+ if t.failed {
+ WriteString("failed");
+ }
+ else {
+ WriteString("ok");
+ }
+
+ WriteString(" (");
+ WriteNumber(t.total_ok);
+ WriteString("/");
+ WriteNumber(t.total_expects);
+ WriteString(")\n");
+}
+
+/// Expect checks the given condition, failing the current test if it is false.
+///
+/// Note: Expect must be called within a test.
+Expect :: (cond: bool, message := "", args: ..Any, loc := #caller_location) #expand {
+ run := `t.(*TestRun);
+ run.total_expects += 1;
+ if cond {
+ run.total_ok += 1;
+ return;
+ }
+
+ msg := "expectation failed";
+ if message.count != 0 {
+ msg = basic.tprint(message, ..args);
+ }
+
+ run.failed = true;
+ if #compile_time {
+ CompileError(msg, loc = loc);
+ }
+ else {
+ WriteStderrLocation(loc);
+ WriteStderrString(": ", msg, "\n");
+ }
+}
+
+
+#scope_file
+
+TestRun :: struct {
+ location: Source_Code_Location;
+ total_expects: s64;
+ total_ok: s64;
+ failed: bool;
+}
+
+basic :: #import "Basic"; // @future
+strings :: #import "String"; // @future
+compiler :: #import "Compiler"; // @future
diff --git a/module.jai b/module.jai
index eb9858c..6f36c10 100644
--- a/module.jai
+++ b/module.jai
@@ -1,26 +1,15 @@
-#scope_export;
+/*
+ Module jc contains procedures for working with memory,
+ arrays, and strings; as well as helpful macros and
+ constants.
-byte :: u8;
-f32 :: float32;
-f64 :: float64;
+ Additionally, it provides a platform-independant
+ interface for interacting with the target operating
+ system.
+*/
-cstring :: *byte;
-rawptr :: *void;
-
-#if size_of(int) == size_of(s64) {
- sint :: s64;
- uint :: u64;
-}
-else {
- sint :: s32;
- uint :: u32;
-}
-
-#if size_of(rawptr) == size_of(u64) {
- uptr :: u64;
- sptr :: s64;
-}
-else {
- uptr :: u32;
- sptr :: s32;
-}
+#load "internal/builtin.jai";
+#load "internal/array.jai";
+#load "internal/memory.jai";
+#load "internal/testing.jai";
+#load "internal/keywords.jai";