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); + + 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, "

%2

", 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, ""); + append(b, "
"); + } + + append(*b, "
"); + append(*b, "

Index

"); + + 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, ""); + 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";