Author:ptrace
Comitter:ptrace
Date:2026-08-17 07:10:05 UTC
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..8f189f8
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,4 @@
*.so
*.o
.build/
bin/
diff --git a/bash/cprof.sh b/bash/cprof.sh
new file mode 100644
index 0000000..779cfc9
--- /dev/null
+++ b/bash/cprof.sh
@@ -0,0 +1,28 @@
#!/bin/bash
# Creates profiling graphs as png in your current directory.
#
# You'll need:
# - gprof2dot
# - dot
bin="$1"
shift
out="${bin}.callgrind"
if [[ -z "$bin" || ! -x "$bin" ]]; then
echo "Usage: cprof <executable> [program args]"
exit 1
fi
valgrind --tool=callgrind \
--callgrind-out-file="$out" \
"$bin" "$@" \
|| echo "valgrind non-zero. Continuing..."
gprof2dot "$out" --format=callgrind -o out.dot || exit 1
dot -Tpng out.dot -o graph.png || exit 1
echo "done"
diff --git a/jai/hot_reload/README.md b/jai/hot_reload/README.md
new file mode 100644
index 0000000..9c5a8b6
--- /dev/null
+++ b/jai/hot_reload/README.md
@@ -0,0 +1,29 @@
# Code Hot Reloading with Context Passing
This is a demonstration how to achieve hot reloading while passing the context.
The idea behind this is, that if using hot reloading, the main program uses the context
and memory allocators from the host, thus preserving state.
## Usage
Hot reload demonstration:
```
jai build.jai - run silent hotreload
```
Then you can edit the `src/main.jai` file and watch the live changes.
For just compiling & running the main file, without hotreloading:
```
jai build.jai - run silent
```
Only rebuild the main file as so, without using the host:
```
jai build.jai - silent hotreload rebuild
```
diff --git a/jai/hot_reload/build.jai b/jai/hot_reload/build.jai
new file mode 100644
index 0000000..1686dcf
--- /dev/null
+++ b/jai/hot_reload/build.jai
@@ -0,0 +1,228 @@
program_args: []string;
args_help: bool;
args_compiler_silent: bool;
args_program_run: bool;
args_build_release: bool;
args_memory_debug: bool;
args_ua_alloc: bool;
args_hot_reload: bool;
args_rebuild_editor: bool;
Release_Kind :: enum_flags {
DEBUG;
SHARED;
RELEASE;
}
Program :: struct {
name: string;
workspace: Workspace;
}
build :: () {
set_build_options_dc(.{ do_output = false });
args := get_build_options().compile_time_command_line;
// args
args_help = array_find(args, "help");
args_compiler_silent = array_find(args, "silent");
args_program_run = array_find(args, "run");
args_build_release = array_find(args, "release");
args_memory_debug = array_find(args, "memory");
args_ua_alloc = array_find(args, "ua");
args_hot_reload = array_find(args, "hotreload");
args_rebuild_editor = array_find(args, "rebuild");
// program args
program_args := program_args_collect(args);
/** Workspaces */
workspace_host := create_workspace("host");
workspace_editor := create_workspace("editor");
if args_help {
args_help_print();
return;
}
make_directory_if_it_does_not_exist("bin");
make_directory_if_it_does_not_exist("bin/debug");
make_directory_if_it_does_not_exist("bin/release");
make_directory_if_it_does_not_exist("bin/debug_hot");
make_directory_if_it_does_not_exist("bin/release_hot");
release_as: Release_Kind = ifx args_build_release then .RELEASE else .DEBUG;
if args_hot_reload then release_as |= .SHARED;
should_run_program := args_program_run ^ (args_hot_reload & args_program_run);
workspace_setup(workspace_editor, run=should_run_program, release_as, #code {
add_build_string(tprint("IS_HOT_RELOAD :: %;", args_hot_reload), workspace);
add_build_file(tprint("%src/main.jai", #filepath), workspace);
});
if args_hot_reload && !args_rebuild_editor {
workspace_setup(workspace_host, run=true, .DEBUG, #code {
add_build_string(
tprint("FP_EDITOR_SO :: \"%\";", "bin/debug_hot/editor.so"), workspace
);
add_build_file(tprint("%src/host.jai", #filepath), workspace);
});
}
}
build_debug :: (w: Workspace, target_options: *Build_Options) {
log("Choosing debug options...");
target_options.backend =.X64;
target_options.output_path = "bin/debug";
set_optimization(target_options, Optimization_Type.DEBUG, true);
set_build_options(target_options.*, w);
}
build_release :: (w: Workspace, target_options: *Build_Options) {
log("Choosing release options...");
target_options.backend = .LLVM;
target_options.output_path = "bin/release";
set_optimization(target_options, Optimization_Type.VERY_OPTIMIZED);
set_build_options(target_options.*, w);
}
build_dynamic_debug :: (w: Workspace, target_options: *Build_Options) {
log("Choosing dyn debug options ...");
target_options.backend = .X64;
target_options.output_type = .DYNAMIC_LIBRARY;
target_options.output_path = "bin/debug_hot";
set_optimization(target_options, Optimization_Type.DEBUG, true);
set_build_options(target_options.*, w);
}
build_dynamic_release :: (w: Workspace, target_options: *Build_Options) {
log("Choosing dyn release options ...");
target_options.backend = .LLVM;
target_options.output_type = .DYNAMIC_LIBRARY;
target_options.output_path = "bin/release_hot";
set_optimization(target_options, Optimization_Type.VERY_OPTIMIZED);
set_build_options(target_options.*, w);
}
args_help_print :: () {
help_message := #string _END_
Usage: jai build.jai - [OPTIONS] :: [PROGRAM ARGS]
Options:
help Prints this help menu.
silent Disables compiler/linker statistics.
run Runs your program afterwards.
release Builds with release options. If omitted, it builds a debug build.
memory Enables the memory leak detector.
Passing Args to your Program:
If you want to supply args to your program, pass it like that:
`jai build.jai - run :: my_arg1 foo bar abc ABC`
Everything after the `::` get's forwarded to your program.
_END_;
log(help_message);
}
program_args_collect :: (args: []string, divider: string = "::") -> [..]string {
buf: [..]string;
success, match := array_find(args, divider);
if success for i: match+1..args.count-1 array_add(*buf, args[i]);
return buf;
}
message_loop :: () -> success: bool {
while true {
message := compiler_wait_for_message();
if !message break;
if message.kind == {
case .COMPLETE;
message_complete := cast(*Message_Complete) message;
return message_complete.error_code == 0;
}
}
return false;
}
create_workspace :: (name: string) -> Program #expand {
w := compiler_create_workspace(name);
if !w{
log("Workspace creation failed: %", name);
`return;
}
return { name, w };
}
workspace_setup :: (using program: Program, run: bool, release_kind: Release_Kind, $intercept: Code) #expand {
insert_constant :: (label: string, any: Any) #expand {
add_build_string(tprint("% :: %;", label, any), `workspace);
}
print("The workspace w is %\n", workspace);
target_options := get_build_options(workspace);
target_options.output_executable_name = name;
if args_compiler_silent target_options.text_output_flags = 0;
if release_kind == {
case .RELEASE;
build_release(workspace, *target_options);
case (.SHARED | .RELEASE);
build_dynamic_release(workspace, *target_options);
case (.SHARED | .DEBUG);
build_dynamic_debug(workspace, *target_options);
case .DEBUG;
build_debug(workspace, *target_options);
case;
compiler_report("Unkown release kind");
}
compiler_begin_intercept(workspace);
#insert,scope() intercept;
insert_constant("IS_DEVELOPER", !args_build_release);
insert_constant("IS_UA_ALLOCATOR", args_ua_alloc);
insert_constant("MEMORY_DEBUGGER_ENABLED", args_memory_debug);
compiler_response := message_loop();
compiler_end_intercept(workspace);
if !compiler_response {
log("Compiler response failed.");
return;
}
if run then run_build_result_of_workspace(workspace, program_args);
}
main :: () {}
#run build();
#import "Basic";
#import "Compiler";
#import "File";
#import "String";
#import "Autorun";
#import "Print_Vars";
diff --git a/jai/hot_reload/modules/0x2_Math.jai b/jai/hot_reload/modules/0x2_Math.jai
new file mode 100644
index 0000000..7469a26
--- /dev/null
+++ b/jai/hot_reload/modules/0x2_Math.jai
@@ -0,0 +1,60 @@
roundf :: inline (x: float) -> float {
return floor(x + 0.5);
}
round :: inline (x: float) -> int {
return xx floor(x + 0.5);
}
#scope_file
using, only(floor) Math :: #import "Math";
/*
------------------------------------------------------------------------------
This software is available under 2 licenses -- choose whichever you prefer.
------------------------------------------------------------------------------
ALTERNATIVE A - MIT License
Copyright (c) 2026 Adam Blazeowsky
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
------------------------------------------------------------------------------
ALTERNATIVE B - Public Domain (www.unlicense.org)
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or distribute this
software, either in source code form or as a compiled binary, for any purpose,
commercial or non-commercial, and by any means.
In jurisdictions that recognize copyright laws, the author or authors of this
software dedicate any and all copyright interest in the software to the public
domain. We make this dedication for the benefit of the public at large and to
the detriment of our heirs and successors. We intend this dedication to be an
overt act of relinquishment in perpetuity of all present and future rights to
this software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
------------------------------------------------------------------------------
*/
diff --git a/jai/hot_reload/src/host.jai b/jai/hot_reload/src/host.jai
new file mode 100644
index 0000000..809a675
--- /dev/null
+++ b/jai/hot_reload/src/host.jai
@@ -0,0 +1,193 @@
lib: *void;
editor_main: Editor_Main;
thread: *Thread;
watcher: File_Watcher;
poll_fds: [1]pollfd;
do_hot_reload: bool;
WATCH_DIR :: "src";
WATCH_FILE :: "main.jai";
FILE_WATCHER_LOG :: false;
MAX_RETRY_DEFAULT :: 20;
DELAY_MS_DEFAULT :s32: 500;
DELAY_MS_MAX :s32: 8000;
BLOCK_TILL_EVENT :: -1;
MAX_WAIT_SECONDS :: 20_000.0;
Editor_Main :: #type (ctx: *#Context);
main :: () {
log("Hello from Host");
log("FP: %", FP_EDITOR_SO);
log("DEF: %", *context);
log("Temporary_Storage uses % bytes.", context.temporary_storage.total_bytes_occupied);
log("-------------------------------");
if !file_watcher_init(WATCH_DIR) return;
max_retry := MAX_RETRY_DEFAULT;
delay_ms := DELAY_MS_DEFAULT;
needs_wait: bool;
wait_seconds: float64;
/** Initial start */
rebuild_so();
reload_so();
while true {
reset_temporary_storage();
if !needs_wait {
event_fds := poll(poll_fds.data, xx poll_fds.count, BLOCK_TILL_EVENT);
if event_fds < 0 {
action := poll_error(event_fds);
if #complete action == {
case .EXIT; return;
case .RETRY; if exp_backoff() then continue; else return;
case .SIGNAL; continue;
}
}
} else {
fms := wait_seconds * 1000.0;
fms = clamp(fms, 0.0, MAX_WAIT_SECONDS);
assert(fms < MAX_WAIT_SECONDS, "Unexpected high timeout");
if fms == MAX_WAIT_SECONDS {
log("process_changes() returned a high timeout!", flags=.WARNING);
}
ms := cast(s32, round(xx fms));
sleep_milliseconds(ms);
}
_, needs_wait=, wait_seconds= := process_changes(*watcher);
if do_hot_reload {
do_hot_reload = false;
rebuild_so();
reload_so();
}
}
while !thread_is_done(thread) {}
}
rebuild_so :: () {
log("Rebuilding SO");
CMD :: string.[ "jai", "build.jai", "-", "silent", "hotreload", "rebuild" ];
if run_command(..CMD, print_captured_output=true).exit_code != 0 {
exit(1);
}
}
reload_so :: () {
log("Reloading SO");
if lib dlclose(lib);
lib = dlopen(FP_EDITOR_SO, RTLD_NOW);
editor_main = cast(Editor_Main)dlsym(lib, "hotreload_main");
editor_main(*context);
log("Temporary_Storage uses % bytes.", context.temporary_storage.total_bytes_occupied);
}
file_watcher_init :: (dir: string) -> ok: bool = false {
if !file_exists(FP_EDITOR_SO) return;
if !init(
*watcher,
file_change_callback,
events_to_watch = .ALL_EVENTS,
verbose=FILE_WATCHER_LOG)
{
log_error("Could not initialize watcher");
return;
}
if !add_directories(*watcher, dir) {
log_error("Could not watch directory %", FP_EDITOR_SO);
return;
}
poll_fds[0].fd = watcher.inotify_instance;
poll_fds[0].events = POLLIN;
log("INotify armed");
return true;
}
file_change_callback :: (watcher: *File_Watcher(void), change: *File_Change, _: *void) {
if !ends_with(change.full_path, WATCH_FILE) return;
do_hot_reload = true;
}
exp_backoff :: () -> should_continue: bool #expand {
if `max_retry <= 0 {
log_error("POLL: Cannot recover!");
return false;
} else {
log("POLL: Retry attempt left: % with % ms delay",
`max_retry,
`delay_ms,
flags=.WARNING
);
}
sleep_milliseconds(`delay_ms);
`delay_ms *= 2;
`delay_ms = min(`delay_ms, DELAY_MS_MAX);
`max_retry -= 1;
return true;
}
poll_error :: (rc: s32) -> enum { EXIT; RETRY; SIGNAL; } {
if errno() == {
/** The allocation of internal data structures failed but a subsequent request may succeed. */
case EAGAIN;
log("POLL: Internal allocation failed.", flags=.WARNING);
return .RETRY;
/** A signal was caught during poll(). */
case EINTR;
return .SIGNAL;
/** The nfds argument is greater than {OPEN_MAX}, or one of the fd members refers to a
STREAM or multiplexer that is linked (directly or indirectly) downstream from
a multiplexer.
*/
case EINVAL;
log_error("POLL: More FDs then supported. Or something else.");
return .EXIT;
case;
log_error("Unknown poll() error. Maybe your ABI version is more recent. My bad.");
return .EXIT;
}
}
#import "Basic";
#import "String";
#import "File";
#import "File_Watcher";
#import "File_Utilities";
#import "Process";
#import "POSIX";
#import "Thread";
#import "0x2_Math";
diff --git a/jai/hot_reload/src/main.jai b/jai/hot_reload/src/main.jai
new file mode 100644
index 0000000..6b020ab
--- /dev/null
+++ b/jai/hot_reload/src/main.jai
@@ -0,0 +1,34 @@
#if IS_HOT_RELOAD {
#program_export
hotreload_main :: (host_ctx: *#Context) {
assert(host_ctx != null);
context = host_ctx.*;
editor_main();
}
} else {
main :: () {
inline editor_main();
}
}
editor_main :: () {
#if MEMORY_DEBUGGER_ENABLED defer report_memory_leaks();
log("\n+++++++++++++++++++++++");
log("Hello from editor main!");
log("+++++++++++++++++++++++\n\n");
_ := tprint("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
_ := tprint("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
_ := tprint("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
_ := tprint("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
_ := tprint("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
}
#import "Basic"()(
MEMORY_DEBUGGER = MEMORY_DEBUGGER_ENABLED
);
diff --git a/jai/word_wrap/word_wrap.jai b/jai/word_wrap/word_wrap.jai
new file mode 100644
index 0000000..622edf1
--- /dev/null
+++ b/jai/word_wrap/word_wrap.jai
@@ -0,0 +1,88 @@
#import "Basic";
FOO :: #string STR_END
Teeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeest
Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test
AnotherTest AnotherTest AnotherTest AnotherTest
END OF STRING IG
STR_END;
main :: () {
text := word_wrap(FOO, 40);
log("%", text);
}
word_wrap :: (text: string, width: int) -> string {
buf: String_Builder;
init_string_builder(*buf);
idx := 0;
line_len := 0;
while idx < text.count {
if is_space(text[idx]) {
append(*buf, text[idx]);
line_len += 1;
idx += 1;
continue;
}
start := idx;
while idx < text.count && !is_space(text[idx]) {
idx += 1;
}
word_len := idx - start;
if line_len + word_len <= width {
for i: start..idx-1 {
append(*buf, text[i]);
}
line_len += word_len;
} else {
if word_len > width {
remaining := word_len;
pos := start;
while remaining > 0 {
chunk := min(width, remaining);
if line_len != 0 {
append(*buf, "\n");
}
for i: 0..chunk-1 {
append(*buf, text[pos + i]);
}
pos += chunk;
remaining -= chunk;
line_len = chunk;
if remaining > 0 {
append(*buf, "\n");
line_len = 0;
}
}
} else {
append(*buf, "\n");
for i: start..idx-1 {
append(*buf, text[i]);
}
line_len = word_len;
}
}
}
// Minus trailing \n
// b := get_current_buffer(*buf);
// b.count -= 1;
return builder_to_string(*buf);
}