Compare commits

..

2 commits

Author SHA1 Message Date
Talha Qamar 4847aa27c9 Added clean task to Make 2025-02-02 04:49:42 +05:00
Talha Qamar 1b8329ec26 Liblily initial commit 2025-02-02 04:48:33 +05:00
82 changed files with 2693 additions and 1430 deletions

1
.gitattributes vendored
View file

@ -1 +0,0 @@
*.hb linguist-language=hblang

2
.gitignore vendored
View file

@ -1 +1 @@
out/
/target/

View file

@ -1,26 +1,49 @@
# lily
# Lily
an attempt at a cross-platform standard library for hblang.
> [!caution]
> 0.1.x currently only supports ableos target, as the new hblang compiler does not support native compilation yet.
> [!important]
> [!IMPORTANT]
> all features, targets, modules, etc, are provisional and may be subject to change or deletion
# working Features
- heap allocators (Arena)
- system rng
- memory operations (copy, set, move, reverse, equals)
- iterators (bytes)
- logger
- typesystem wrapper
use `./build -h` to see available arguments.
### supports:
- changing target
- custom linker (for native targets)
- running the executable (for native targets)
- setting output path
- dumping assembly representation
- only recompiling if either source or environment change
# TODO Features
- string formatting/interpolation
- printf
- hashers
- random number generators
### To change build target
use the `-t` flag supplied in `./build`
use a target triple (i.e. `x86_64-unknown-linux-gnu`), or pick from one of these aliases:
>[!NOTE]
`hbvm == ableos` (for now)
>- hbvm
>- ableos
>- libc (links to system libc)
### Modifying build config
compiler flags are in: `./build` (at top of file)
> used for NON-CODE configuration
compile-time configuration is in: `./src/lily/lib.hb`
> used for things like toggling debug assertions, setting minimum log level, etc
# Features
features include:
- heap allocator
- system rand
- memory operations
- math operations
- collections (Hashmap, Vec)
- better allocators
- result type
- string operations, including split iteration
- hasher
- hashmap
- vec (dynamic array)
- printing & logging
- result type
- typesystem wrapper
- string formatting & interpolation

21
TODO.md
View file

@ -1,4 +1,17 @@
see [here](<README.md#todo-features>) for feature todos.<br>
this is strictly for dev todos.
- replace `idk` in places with Type(T).uninit()
- seeded prng
- smarter allocator for alloc
- add code comments everywhere
- add docstrings everywhere
- write more of spec & docs
- optimise hashmap implementation
- rehash buckets & squash after size threshold
- write test suite
- report compiler bugs
- once done, verify i have written ok code ૮꒰ ˶• ༝ •˶꒱ა
- make foldhash implementation identical to original
- report MORE compiler bugs!!!
- implement more math stuff
- utilise libc more where possible (free performance, portability, reliability)
- optimise! optimise! optimise!
- confirm current implementations match spec
- heap string type

376
build
View file

@ -1,117 +1,301 @@
#!/bin/sh
set -u
# shellcheck disable=SC2155
readonly LILY_SCRIPT_DIR="$(CDPATH='' cd -- "$(dirname -- "$0")" && pwd)"
readonly LILY_SRC_DIR="${LILY_SRC_DIR:-$LILY_SCRIPT_DIR/src}"
readonly LILY_TEST_DIR="${LILY_TEST_DIR:-$LILY_SCRIPT_DIR/hbc-tests}"
readonly LILY_BUILD_DIR="${LILY_BUILD_DIR:-$LILY_SCRIPT_DIR/out}"
SCRIPT_DIR="$(CDPATH='' cd -- "$(dirname -- "$0")" && pwd)"
readonly SCRIPT_DIR
readonly HBC_COMMIT="18e8a831"
readonly SRC_DIR="$SCRIPT_DIR/src"
readonly TEST_DIR="$SRC_DIR/test"
readonly BUILD_PATH="$SCRIPT_DIR/target"
readonly CHECKSUM_FILE="$BUILD_PATH/checksum"
readonly HBLANG_GIT="https://git.ablecorp.us/mlokis/hblang"
readonly HBLANG_COMMIT="c5a6582d12d62816fcbdbec4d949656fd6d88b39"
readonly HBC_FLAGS="--path-projection lily $LILY_SRC_DIR/lib.hb ${HBC_FLAGS:-}"
readonly HBC_BINARY="hbc"
readonly HBC_TESTS="${HBC_TESTS:-0}"
hbc_flags="--backend-flags=opt_level=none,regalloc_checker=true,enable_verifier=true,use_colocated_libcalls=true,enable_pcc=true,enable_llvm_abi_extensions=true,preserve_frame_pointers=true"
linker="" target="default" run=0 test="none" dump_asm=0 debug=1
die() { error "$2" 1>&2 && exit "$1"; }
error() { printf "\033[31mERROR\033[0m: %b\n" "$1"; }
log() { printf "\033[32mINFO\033[0m: %b\n" "$1"; }
warn() { printf "\033[33mWARN\033[0m: %b\n" "$1"; }
die() { error "$1" && exit 1; }
error() { printf "\033[31mERROR\033[0m: %s\n" "$1" >&2; }
log() { printf "\033[32mINFO\033[0m: %s\n" "$1"; }
warn() { printf "\033[33mWARN\033[0m: %s\n" "$1"; }
fetch_step() {
command -v zig >/dev/null 2>&1 || die 101 "'zig' binary not found in PATH"
command -v git >/dev/null 2>&1 || die 101 "'git' binary not found in PATH"
check_hbc() {
command -v hbc >/dev/null 2>&1 && return 0
log "hblang compiler not found in \$PATH"
command -v cargo >/dev/null 2>&1 || die "missing both hbc and cargo"
hblang_dir="$LILY_BUILD_DIR/hblang"
mkdir -p "$hblang_dir" || die 1 "failed to create hblang directory"
cargo_ver=$(cargo install --list | awk '/holey-bytes/ {if(match($0,/[0-9a-f]+/)) print substr($0,RSTART,8)}')
[ "$cargo_ver" = "$HBC_COMMIT" ] && return 0
if [ -d "$hblang_dir/.git" ]; then
cd "$hblang_dir" || die 1 "failed to enter hblang directory"
log "installing hbc@$HBC_COMMIT..."
cargo +nightly install --git https://git.ablecorp.us/ableos/holey-bytes hbc --debug --rev "$HBC_COMMIT" >/dev/null 2>&1 ||
die "failed to install hbc (do you have nightly rust installed?)"
}
current_commit=$(git rev-parse HEAD 2>/dev/null)
if [ "$current_commit" != "$HBLANG_COMMIT" ]; then
if ! git cat-file -e "$HBLANG_COMMIT^{commit}" 2>/dev/null; then
log "fetching hblang repository updates"
git fetch --all >/dev/null 2>&1 || die 1 "failed to fetch repository"
fi
git checkout -f "$HBLANG_COMMIT" >/dev/null 2>&1 || die 1 "failed to checkout commit"
check_changes() {
previous_checksum=""
[ -r "$CHECKSUM_FILE" ] && previous_checksum=$(cat "$CHECKSUM_FILE")
current_checksum=$( (
find "$SRC_DIR" -type f -exec md5sum {} + | LC_ALL=C sort
printf "%s%s%s" "$linker" "$target" "$hbc_flags"
) | md5sum)
echo "$current_checksum" >"$CHECKSUM_FILE"
[ "$previous_checksum" != "$current_checksum" ]
}
show_help() {
cat <<EOF
Usage: $0 [options]
-r, --run run the output binary
-n, --no-debug disable debug mode
-a, --dump-asm dump assembly to stdout
-l, --linker specify linker (default: auto-detect)
-t, --target target triple/alias (<default>, libc, ableos, hbvm)
-h, --help show this help
-u, --test run tests (lang/lily/<all>)
Examples:
$0 -t ableos
$0 -r -l 'zig cc' -t libc
$0 -a -t ableos > out.hbf
EOF
}
parse_args() {
while [ $# -gt 0 ]; do
case ${1:-""} in
-h | --help)
show_help
exit
;;
-r | --run) run=1 ;;
-n | --no-debug) debug=0 hbc_flags="--backend-flags=opt_level=speed,enable_verifier=false,regalloc_checker=false,enable_alias_analysis=true,use_colocated_libcalls=true,enable_llvm_abi_extensions=true" ;;
-a | --dump-asm) dump_asm=1 ;;
-t | --target)
[ -z "${2:-}" ] && die "'--target' requires a non-empty option argument"
target=$2
shift
;;
-u | --test)
test="all"
;;
--test=?*)
if [ "${1#*=}" = "none" ]; then warn "'--test=none' is redundant"; fi
test=${1#*=}
;;
--test=)
die "'--test=' requires a non-empty option argument"
;;
--target=?*) target=${1#*=} ;;
--target=)
die "'--target=' requires a non-empty option argument"
;;
-l | --linker)
[ -z "${2:-}" ] && die "'--linker' requires a non-empty option argument"
linker=$2
shift
;;
--linker=?*) linker=${1#*=} ;;
--linker=) die "'--linker' requires a non-empty option argument" ;;
-o | --out)
[ -z "${2:-}" ] && die "'--out' requires a non-empty option argument"
out=$2
shift
;;
--out=?*) out=${1#*=} ;;
--out=) die "'--out' requires a non-empty option argument" ;;
--)
shift
break
;;
-?*) die "unknown option $1" ;;
*) break ;;
esac
shift
done
case $target in
hbvm | ableos)
target="unknown-virt-unknown"
if [ $run = 1 ]; then die "'--run' cannot be set with '--target $target'"; fi
;;
libc | default) target="default" ;;
esac
}
build_failed() {
rm -f "$BUILD_PATH/$target/lily" "$CHECKSUM_FILE"
die "build failed"
}
detect_linker() {
for cmd in "zig cc" clang gcc; do
command -v "${cmd%% *}" >/dev/null 2>&1 && {
linker=$cmd
return
}
done
die "no suitable linker found"
}
post_build() {
[ "$target" = "unknown-virt-unknown" ] && return
[ $run -eq 1 ] && exec "$BUILD_PATH/$target/lily"
[ $dump_asm -eq 1 ] && objdump -d -M intel --no-show-raw-insn "$BUILD_PATH/$target/lily.o" |
awk '/^\s+[0-9a-f]+:/ {sub(/^[ \t]+/, ""); print}'
}
parse_test() {
in_comment=0 exit_code="" args_input="" stdout_expected="" timeout_val=""
while IFS= read -r line; do
case $in_comment in
0) case $line in "/*"*)
in_comment=1
line=${line#/*}
;;
esac ;;
1) case $line in *"*/"*)
line=${line%*/}
in_comment=2
;;
esac ;;
esac
line=$(echo "$line" | sed 's/^[[:space:]]*\*[[:space:]]\{0,1\}//; s/[[:space:]]*$//')
[ $in_comment -ne 0 ] && process_test_line "${line%%*/}"
[ $in_comment -eq 2 ] && break
done <"$1"
}
process_test_line() {
line=$(echo "$1" | sed 's/^[[:space:]]*\*[[:space:]]*//; s/[[:space:]]*$//')
[ -z "$line" ] && return
IFS=: read -r key value <<EOF
$line
EOF
key=$(echo "$key" | tr -d ' ')
value=$(echo "$value" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')
case $key in
exit) exit_code=$value ;;
args) args_input=$value ;;
stdout) stdout_expected=$value ;;
timeout) timeout_val=${value} ;;
esac
}
run_test() {
file=$1 name=$2 output="" rc=0 failed=0
parse_test "$file"
hbc "$file" $hbc_flags >"$BUILD_PATH/test/$name.o" 2>&1
rc=$?
[ $rc -ne 0 ] && {
# todo: quiet mode to hide compilation errors.
error "$name: compilation failed (exit: $(pretty_exitcode "$rc")): $(cat "$BUILD_PATH/test/$name.o")"
return 1
}
[ $debug -eq 0 ] && {
strip --strip-all --strip-debug "$BUILD_PATH/$target/lily.o"
}
$linker "$BUILD_PATH/test/$name.o" -o "$BUILD_PATH/test/$name" || die "linking failed:"
if [ -n "$timeout_val" ]; then
# shellcheck disable=SC2086
output=$(timeout "$timeout_val" "$BUILD_PATH/test/$name" $args_input 2>&1)
rc=$?
[ $rc -eq 124 ] && {
error "$name: timeout"
failed=1
}
else
# shellcheck disable=SC2086
output=$("$BUILD_PATH/test/$name" $args_input 2>&1)
rc=$?
fi
else
log "cloning hblang repository"
git clone "$HBLANG_GIT" "$hblang_dir" >/dev/null 2>&1 || die 1 "failed to clone repository"
cd "$hblang_dir" || die 1 "failed to enter cloned directory"
if ! git cat-file -e "$HBLANG_COMMIT^{commit}" 2>/dev/null; then
git fetch origin "$HBLANG_COMMIT" >/dev/null 2>&1 || die 1 "failed to fetch commit"
[ "$rc" != "$exit_code" ] && {
error "$name: exit code $(pretty_exitcode "$rc") != $exit_code"
failed=1
}
[ -n "$stdout_expected" ] && [ "$output" != "$stdout_expected" ] && {
error "$name: output mismatch
expected: $stdout_expected
got: $output"
failed=1
}
[ $failed -eq 0 ] && log "$test_name completed successfully"
return $failed
}
pretty_exitcode() {
exit_code=$1
signal=$((exit_code - 128))
if [ $signal -gt 0 ] && [ $signal -lt 32 ]; then
echo "$exit_code (SIG$(kill -l $signal 2>/dev/null || echo "???"))"
else
echo "$exit_code"
fi
git checkout -f "$HBLANG_COMMIT" >/dev/null 2>&1 || die 1 "failed to checkout commit"
fi
}
# do not build with optimisations if you want to report bugs.
zig build install >/dev/null || die $? "failed to build hblang. exit code=$?"
# todo: this spawns every test all at once. consider limiting to $(nproc) tests at once.
do_tests() {
detect_linker
mkdir -p "$BUILD_PATH/test" || die "failed to create test dir"
PATH="$hblang_dir/zig-out/bin:$PATH"
cd "$LILY_SCRIPT_DIR" || die 1 "failed to return to script directory"
tmpfile=$(mktemp) || die "Failed to create temporary file"
find "${TEST_DIR}/${test#all}" -type f >"$tmpfile" || die "Find command failed"
while IFS= read -r file; do
[ -f "$file" ] || continue
test_name=$(basename "$file" .hb)
log "Testing $test_name"
(run_test "$file" "$test_name") &
done <"$tmpfile"
wait
rm -r "$tmpfile" "$BUILD_PATH/test"
}
do_build() {
mkdir -p "$BUILD_PATH/$target" || die "failed to create $BUILD_PATH/$target"
[ -w "$BUILD_PATH/$target" ] || die "no write permission"
if check_changes || [ ! -e "$BUILD_PATH/$target/lily" ]; then
if [ "$target" = "unknown-virt-unknown" ]; then
out="$BUILD_PATH/$target/lily.axe"
[ $dump_asm -eq 1 ] && {
hbc_flags="$hbc_flags --dump-asm"
out="/dev/stdout"
}
# shellcheck disable=SC2086
hbc "$SRC_DIR/main.hb" $hbc_flags --target="$target" >"$out" || build_failed
else
[ -z "$linker" ] && detect_linker
[ "$target" != "default" ] && hbc_flags="$hbc_flags --target=$target"
# shellcheck disable=SC2086
hbc "$SRC_DIR/main.hb" $hbc_flags >"$BUILD_PATH/$target/lily.o" || build_failed
[ $debug -eq 0 ] && {
strip --strip-all --strip-debug "$BUILD_PATH/$target/lily.o"
}
$linker "$BUILD_PATH/$target/lily.o" -o "$BUILD_PATH/$target/lily"
fi
fi
post_build
}
main() {
mkdir -p "$LILY_BUILD_DIR" || die 1 "failed to create build directory"
fetch_step
parse_args "$@"
mkdir -p "$BUILD_PATH" || die "can't create build dir"
check_hbc
do_test() {
# shellcheck disable=SC2086
$HBC_BINARY $HBC_FLAGS --vendored-test "$file" >"$LILY_BUILD_DIR/$test_name.test" 2>&1
exit="$?"
if [ ! $exit = 0 ]; then
error "\e[1;31m\033[0m $test_name \e[1;30m(exit=\e[1;31m$exit\e[1;30m) ($(basename "$LILY_BUILD_DIR")/$test_name.test)"
echo "0" >>"$result_file"
else
# shellcheck disable=SC2086
len=$($HBC_BINARY $HBC_FLAGS "$file" | wc -c | numfmt --to=iec-i)
log "✓ $test_name \e[1;30m(size=\e[1;32m$len\e[1;30m)"
# log "\e[0;32m✓\033[0m $test_name"
echo "1" >>"$result_file"
# surely good idea.
rm "$LILY_BUILD_DIR/$test_name.test"
# shellcheck disable=SC2086
$HBC_BINARY $HBC_FLAGS "$file" --fmt >/dev/null 2>&1
fi
}
if [ "$HBC_TESTS" = 1 ]; then
tmpfile=$(mktemp)
result_file=$(mktemp)
find "$LILY_TEST_DIR" -type f >"$tmpfile"
while IFS= read -r file; do
test_name=$(realpath -s --relative-to="$LILY_TEST_DIR" "$file" | tr '/' '.')
test_name="${test_name%.*}"
do_test 2>/dev/null &
done <"$tmpfile"
rm -f "$tmpfile"
wait
failures=$(grep -c "0" "$result_file")
successes=$(grep -c "1" "$result_file")
rm -f "$result_file"
if [ "$failures" = 0 ]; then
log "$successes/$((successes + failures)) tests passed"
else
warn "\e[0;31m$successes\033[0m/$((successes + failures)) tests passed"
fi
exit
fi
inp="${1:-main.hb}"
[ ! -e "$inp" ] && die 1 "source file '$inp' does not exist"
# shellcheck disable=SC2086
if $HBC_BINARY $HBC_FLAGS "$inp" >"$LILY_BUILD_DIR/out.axe"; then
$HBC_BINARY $HBC_FLAGS "$inp" --fmt >/dev/null 2>&1
else
exit $?
fi
case $test in
none) do_build ;;
lang | lily | all)
[ "$test" = "all" ] && test=""
[ $dump_asm -eq 1 ] && die "'--dump-asm' incompatible with tests (for now)"
[ "$target" != "default" ] && die "'--target' incompatible with tests"
[ $run -eq 1 ] && warn "'--run' is redundant with tests"
do_tests
;;
*) die "unknown test: $test" ;;
esac
return 0
}
main "$@"

View file

@ -1,10 +1,11 @@
# lily specification
> [!important]
> before lily version 1.0.0, the spec is provisional and may be subject to change.
> [!Important] spec version: 0.0.2
> before version 1.0.0, the spec is provisional and may be subject to change.
a collection of guidelines for programmers to use to create lily-compatible implementations.
the following files define the spec:
- [hashers](./spec/hash.md)
- [allocators](./spec/alloc.md)
- [random number generators](./spec/rand.md)
- [iterators](./spec/iter.md)
- [tests](./spec/tests.md)

View file

@ -1,10 +1,11 @@
# allocators
> [!tip]
well designed allocators should ensure they only deallocate or reallocate allocations they made.
1. all spec compliant allocators should implement:
> unless otherwise stated, functions can be optionally inline.<br>
> names of arguments are up to programmer discretion.<br>
> names and signature of functions must be identical to shown below.
```rust
Allocator := struct {
new := fn(): Self
@ -12,15 +13,15 @@ Allocator := struct {
deinit := fn(self: ^Self): void
/// should return null on failure.
/// should dealloc any intermediate allocations on failure.
alloc := fn(self: ^Self, $T: type, count: uint): ?[]T
alloc := fn(self: ^Self, $T: type, count: uint): ?^T
/// same behaviour as alloc, except:
/// must be zeroed.
alloc_zeroed := fn(self: ^Self, $T: type, count: uint): ?[]T
alloc_zeroed := fn(self: ^Self, $T: type, count: uint): ?^T
/// same behaviour as alloc, except:
/// must move data to new allocation,
/// must ensure the old allocation is freed at some point.
realloc := fn(self: ^Self, $T: type, prev: []T, new_count: uint): ?[]T
realloc := fn(self: ^Self, $T: type, ptr: ^T, new_count: uint): ?^T
/// must dealloc or schedule the freeing of the given allocation
dealloc := fn(self: ^Self, $T: type, prev: []T): void
dealloc := fn(self: ^Self, $T: type, ptr: ^T): void
}
```

24
docs/spec/hash.md Normal file
View file

@ -0,0 +1,24 @@
# hashers
1. hashers must not allocate on the heap.
2. all spec compliant hashers should implement:
> unless otherwise stated, functions can be optionally inline.<br>
> names of arguments are up to programmer discretion.<br>
> names and signature of functions must be identical to shown below.
```rust
Hasher := struct {
new := fn(seed: SeedType): Self
/// prepare to be deallocated
deinit := fn(self: ^Self): void
/// should always use a constant or randomised seed
/// randomised seeds should (for now) use lily.Target.getrandom()
/// never use values from the environment
default := fn(): Self
/// pointers: treat as uint
/// slices: read bytes and hash
/// other: hash bytes directly
write := fn(self: ^Self, any: @Any()): void
/// should not reset the state of the hasher
finish := fn(self: ^Self): uint
reset := fn(self: ^Self): void
}
```

View file

@ -1,11 +1,20 @@
# iterators
## spec to-be-defined
> [!note]
> spec tbd
## proposed spec:
```rust
iter.{Iterator, Next} := @use("lily").iter
.{IterNext, Iterator} := @use("lily").iter
Iterable := struct {
next := fn(self: ^Self): Next(T)
// ! required
// IterNext == struct { finished: bool, val: T }
next := fn(self: ^Self): IterNext(T)
into_iter := fn(self: Self): Iterator(Self)
// ! proposed
// addition of these two would allow for cheap implementation of `skip`, and `nth` in lily.iter.Iterator
peek := fn(self: ^Self): IterNext(T)
advance := fn(self: ^Self, n: uint): IterNext(T)
// ! proposed (waiting on compiler)
iter := fn(self: ^Self): Iterator(^Self)
}

31
docs/spec/rand.md Normal file
View file

@ -0,0 +1,31 @@
# random number generators
1. rngs must not allocate on the heap.
2. the seed of the rng must not change (unless required by the algorithm)
3. all spec compliant rngs should implement:
> unless otherwise stated, functions can be optionally inline.<br>
> names of arguments are up to programmer discretion.<br>
> names and signature of functions must be identical to shown below.
```rust
Random := struct {
new := fn(seed: uint): Self
/// prepare to be deallocated
deinit := fn(self: ^Self): void
/// should always use a constant or randomised seed
/// randomised seeds should always use lily.Target.getrandom()
/// never use values from the environment
default := fn(): Self
/// array|slice: compile error
/// pointer: treat as uint
/// bool: randomise the least significant bit
/// other: randomise
any := fn(self: ^Self, $T: type): T
/// clamp between min and max (inclusive)
range := fn(self: ^Self, min: @Any(), max: @TypeOf(min)): @TypeOf(min)
/// ^array: fill to @lenof(@ChildOf(@TypeOf(buf))) with random bytes
/// slice: fill to buf.len with random bytes
/// other: compile error
fill := fn(self: ^Self, buf: @Any()): void
/// same as fill, except fill each index with a random value rather than filling the whole buffer with random bytes
fill_values := fn(self, ^Self, buf: @Any()): void
}
```

View file

@ -1,22 +1,38 @@
# tests
tests are written in [src/test](../../src/test/)
1. tests that test the language will be in the [lang](../../src/test/lang) subdirectory
2. tests that test lily will be in the [lily](../../src/test/lily) subdirectory
3. all tests should return `u8`, `void`, or `bool` as a status code.
> follow standard status code practices for value.<br>
> `0` for success, `1` for error, etc
4. all tests should contain the test specification at the top of the file
- all test arguments are optional
> tests with no arguments will always pass
- delimiting whitespace is optional, provided each test argument is prepended with a newline and a `*`
- `timeout` is formatted like `0.5s`. if a test exceeds this time, it will fail.
- `args` are given to the application executable
- `exit` is the exit status of the program (return code)
5. if test compilation fails, the test will be considered failed
6. tests for lily should try to limit the number of unrelated structures/functions tested.
7. tests for lang should be headless (not rely on lily at all)
1. tests should attempt to be meaningful in some way, patching targeting bugs, or behaviours.
the following are all of the (currently) supported test arguments:
```rust
/*
* exit: u8
* args: str
* stdout: str
* timeout: time
*/
```
a trivial example test:
```rust
/*
* timeout: 0.5s
*/
2. tests should be written as hblang vendored tests. all tests can be run with `HBC_TESTS=1 ./build`. status of failed tests should be emitted to `out/subdir.test-name.test`
3. tests should be written as one of:
- part of an exhaustive folder covering all aspects of a datastructure / module. e.g:
```
lily/
some-module/
func1.hb
struct/
method1.hb
method2.hb
other-module/
...
...
lang/
...
```
- one-and-done tests for locating bugs (which should be kept after the bug is patched, unless it is made obsolete by an exhaustive test)
main := fn(): void {
// test will fail after 0.5s
loop {}
}
```

View file

@ -1,11 +0,0 @@
expectations := .{
return_value: 0,
}
func := fn(a: @Any(), b: @TypeOf(a)): uint {
return 0
}
main := fn(): uint {
return func(@as(uint, 1), 2)
}

View file

@ -1,15 +0,0 @@
expectations := .{
return_value: 0,
}
A := struct {
apply := fn(self: ^@CurrentScope(), $func: type): uint {
return func()
}
}
main := fn(): uint {
return A.().apply(fn(): uint {
return 0
})
}

View file

@ -1,18 +0,0 @@
expectations := .{
return_value: 0,
}
generic := fn(v: @Any()): uint {
if @TypeOf(v) == uint {
return 1
}
return 0
}
main := fn(): u8 {
a := generic(0)
b := generic(@as(int, 0))
if a != 1 return 1
if b != 0 return 1
return 0
}

View file

@ -1,11 +0,0 @@
expectations := .{
return_value: 0,
}
hex := fn(): uint return 0x2D
dec := fn(): uint return 45
main := fn(): uint {
if hex() != dec() return 1
return 0
}

View file

@ -1,17 +0,0 @@
expectations := .{
return_value: 0,
}
index := fn(buf: @Any()): u8 {
b := buf[0]
buf[0] = 1
return b
}
main := fn(): u8 {
_ = @inline(index, u8.[0, 0, 0])
_ = @inline(index, u8.[0, 0, 0][0..3])
_ = index(u8.[0, 0, 0])
_ = index(u8.[0, 0, 0][0..3])
return 0
}

View file

@ -1,19 +0,0 @@
expectations := .{
times_out: true,
}
opaque := fn(): ^u8 {
return @bit_cast(1)
}
inner := fn(ptr: ^u8): void {
if ptr != @bit_cast(1) die
}
main := fn(): void {
ptr := opaque()
loop {
inner(ptr)
}
}

View file

@ -1,15 +0,0 @@
expectations := .{
return_value: 0,
}
secondary := fn(): uint {
return inlined()
}
$inlined := fn(): uint {
return 0
}
main := fn(): uint {
return secondary()
}

View file

@ -1,22 +0,0 @@
expectations := .{
return_value: 0,
}
opaque := fn(v: @Any()): bool {
return v < 0
}
opaque2 := fn(v: @Any()): bool {
return v > 0
}
main := fn(): u8 {
v: int = -10
if !opaque(v) return 1
if !@inline(opaque, v) return 1
if opaque2(v) return 1
if @inline(opaque2, v) return 1
return 0
}

View file

@ -1,19 +0,0 @@
expectations := .{
return_value: 0,
}
Kind := enum {
.Builtin;
.Pointer;
}
kind := fn($T: type): Kind {
return @bit_cast(@kind_of(T))
}
main := fn(): uint {
$match kind(uint) {
.Builtin => return 0,
_ => return 1,
}
}

View file

@ -1,10 +0,0 @@
expectations := .{
return_value: 0,
}
main := fn(): uint {
ptr0: ^u8 = @bit_cast(0)
ptr1 := ptr0 + 100
if ptr1 != @bit_cast(100) return 1
return 0
}

View file

@ -1,25 +0,0 @@
expectations := .{
return_value: 0,
}
Vec := struct {
.len: uint
new := fn(): Vec {
return .(10)
}
pop := fn(self: ^Vec): u8 {
self.len -= 1
return 0
}
}
main := fn(): u8 {
vec := Vec.new()
len := vec.len
i: u8 = 0
loop if vec.len == 0 break else {
defer i += 1
_ = vec.pop()
}
return i != len
}

View file

@ -1,22 +0,0 @@
expectations := .{
return_value: 0,
}
Struct := struct {
.inner: ?^u8
modify := fn(self: ^Struct, $T: type): void {
self.inner = @bit_cast(1)
}
}
main := fn(): uint {
a := Struct.(null)
a.modify(void)
if a.inner == null return 1
b := Struct.(null)
Struct.modify(&b, void)
if b.inner == null return 1
return 0
}

View file

@ -1,23 +0,0 @@
expectations := .{
return_value: 0,
}
opaque := fn(): ?^u8 {
return null
}
$transparent := fn(): ?^u8 {
return null
}
main := fn(): u8 {
result := opaque()
if result != null {
return 1
}
result2 := transparent()
if result2 != null {
return 1
}
return 0
}

View file

@ -1,7 +0,0 @@
expectations := .{
return_value: 0,
}
main := fn(): u8 {
return 0
}

View file

@ -1,13 +0,0 @@
expectations := .{
return_value: 0,
}
TypeOf := fn(v: @Any()): type return Generic(@TypeOf(v))
Generic := fn($T: type): type return struct{.inner: T}
main := fn(): uint {
$if TypeOf(@as(uint, 1)) != Generic(uint) {
return 1
}
return 0
}

View file

@ -1,17 +0,0 @@
expectations := .{
return_value: 5,
emulate_ecalls: true,
}
lily.{mem, alloc} := @use("../../src/lib.hb")
main := fn(): uint {
arena := alloc.Arena.new()
defer arena.deinit()
_ = arena.alloc(u8, 1).?
iter := mem.iter(mem.reverse("Hello, World!")[1..]).take(5)
str := iter.collect_vec(&arena)
return str.len()
}

View file

@ -1,19 +0,0 @@
expectations := .{
return_value: 0,
emulate_ecalls: true,
}
lily.{fmt} := @use("../../src/lib.hb")
scratch: [4096]u8 = idk
main := fn(): uint {
len := fmt.fmt_int(scratch[..], 4096, 10)
if len != 4 return 1
if scratch[0] != '4' | scratch[1] != '0' | scratch[2] != '9' | scratch[3] != '6' return 1
len = fmt.fmt_int(scratch[..], 5050, 16)
if len != 6 return 1
if scratch[0] != '0' | scratch[1] != 'x' | scratch[2] != '1' | scratch[3] != '3' | scratch[4] != 'B' | scratch[5] != 'A' return 1
return 0
}

View file

@ -1,17 +0,0 @@
expectations := .{
return_value: 0,
emulate_ecalls: true,
}
lily.{mem} := @use("../../src/lib.hb")
A := struct{.a: u8 = 51; .b: u8 = 42; .c: u8 = 77}
main := fn(): uint {
bytes := mem.as_bytes(&A.{})
if bytes.len != 3 return 1
if bytes[0] != 51 return 51
if bytes[1] != 42 return 42
if bytes[2] != 77 return 77
return 0
}

View file

@ -1,14 +0,0 @@
expectations := .{
return_value: 0,
}
lily.{mem} := @use("../../src/lib.hb")
main := fn(): uint {
abc := "abc"
a_b_c := u8.['a', 'b', 'c'][..]
if !mem.equals(abc, abc) return 1
if !mem.equals(a_b_c, abc) return 1
return 0
}

View file

@ -1,29 +0,0 @@
expectations := .{
return_value: 0,
}
lily.{Type} := @use("../../src/lib.hb")
main := fn(): uint {
$match Type([10]uint).kind() {
.Array => {
},
_ => return 1,
}
$match Type(main).kind() {
.Function => {
},
_ => return 2,
}
$match Type(uint).kind() {
.Builtin => {
},
_ => return 3,
}
$match Type([]u8).kind() {
.Slice => {
},
_ => return 3,
}
return 0
}

View file

@ -1,17 +0,0 @@
expectations := .{
return_value: 0,
}
lily.{TypeOf} := @use("../../src/lib.hb")
dependent := fn(v: @Any()): uint {
$T := TypeOf(v)
$match T.kind() {
.Builtin => return 0,
_ => return 1
}
}
main := fn(): uint {
return dependent(@as(uint, 100))
}

10
main.hb
View file

@ -1,10 +0,0 @@
lily.{fmt, log, mem, alloc, target} := @use("lily")
main := fn(): void {
arena := alloc.Arena.new()
defer arena.deinit()
iter := mem.iter(mem.reverse("Hello, World!")[1..]).take(5)
str := iter.collect_vec(&arena)
log.info(str.slice)
}

View file

@ -1,85 +0,0 @@
lily.{target, mem} := @use("../lib.hb")
AllocationHeader := struct {
.cap: uint;
.len: uint;
.next: ?^Self
Self := @CurrentScope()
$new := fn(size: uint): ?^Self {
total_size := size + @size_of(Self)
ptr: ?^Self = @bit_cast(target.alloc(total_size))
if ptr == null return null
header: ^Self = @bit_cast(ptr)
header.* = .(
target.pages(total_size) * target.page_len() - @size_of(Self),
0,
null,
)
return header
}
}
Arena := struct {
.allocation: ?^AllocationHeader
Self := @CurrentScope()
$new := fn(): Self {
return .(null)
}
alloc := fn(self: ^Self, $T: type, count: uint): ?[]T {
size := mem.size(T, count)
header: ^AllocationHeader = @bit_cast(self.allocation)
if self.allocation == null {
new_header := AllocationHeader.new(size)
// todo: handle cleanly
if new_header == null die
self.allocation = new_header
header = @bit_cast(new_header)
}
loop {
if header.len + size <= header.cap {
header.len += size
break
}
if header.next == null {
header.next = AllocationHeader.new(size)
// todo: handle cleanly
if header.next == null die
}
header = @bit_cast(header.next)
}
return @as(^T, @bit_cast(@as(^u8, @bit_cast(header + 1)) + header.len - size))[0..count]
}
$alloc_zeroed := fn(self: ^Self, $T: type, count: uint): ?[]T {
slice := self.alloc(T, count)
if slice == null return null
mem.set(mem.as_bytes(slice.?), 0)
return slice
}
$realloc := fn(self: ^Self, $T: type, prev: []T, count_new: uint): ?[]T {
slice := self.alloc(T, count_new)
if slice == null return null
mem.copy(mem.as_bytes(slice.?), mem.as_bytes(prev))
return slice
}
$dealloc := fn(self: ^Self, $T: type, prev: []T): void {
}
deinit := fn(self: ^Self): void {
if self.allocation == null {
return
}
allocation: ^AllocationHeader = @bit_cast(self.allocation)
loop {
next := allocation.next
target.dealloc(@bit_cast(allocation), allocation.cap + @size_of(AllocationHeader))
if next == null break
allocation = @bit_cast(next)
}
self.allocation = null
}
}

View file

@ -1,2 +0,0 @@
arena.{Arena} := @use("arena.hb")
vec.{Vec} := @use("vec.hb")

View file

@ -1,43 +0,0 @@
Vec := fn(T: type, A: type): type return struct {
.slice: []T;
.cap: uint;
.allocator: ^A
Self := @CurrentScope()
$new := fn(allocator: ^A): Self {
return .(&.[], 0, allocator)
}
push := fn(self: ^Self, elem: T): void {
if self.slice.len == self.cap {
if self.cap == 0 {
new_slice := self.allocator.alloc(T, 1)
if new_slice == null {
// todo: handle
die
}
self.slice.ptr = new_slice.?.ptr
self.cap = new_slice.?.len
} else {
new_slice := self.allocator.realloc(T, self.slice[0..self.cap], self.cap * 2)
if new_slice == null {
// todo: handle
die
}
self.slice.ptr = new_slice.?.ptr
self.cap = new_slice.?.len
}
}
self.slice[self.slice.len] = elem
self.slice.len += 1
}
$len := fn(self: ^Self): uint {
return self.slice.len
}
deinit := fn(self: ^Self): void {
self.allocator.dealloc(T, self.slice[0..self.cap])
self.* = idk
}
}

View file

@ -1,92 +0,0 @@
.{target, Type, TypeOf, mem} := @use("lib.hb")
fmt_int := fn(buf: []u8, v: @Any(), radix: @TypeOf(v)): uint {
if radix == 0 {
mem.copy(buf, mem.as_bytes(&v))
return @size_of(@TypeOf(v))
}
prefix_len := 0
if Type(@TypeOf(v)).is_signed_int() & v < 0 {
v = -v
buf[0] = '-'
prefix_len += 1
}
if radix == 16 {
mem.copy(buf[prefix_len..], "0x")
prefix_len += 2
} else if radix == 8 {
mem.copy(buf[prefix_len..], "0o")
prefix_len += 2
} else if radix == 2 {
mem.copy(buf[prefix_len..], "0b")
prefix_len += 2
}
if v == 0 {
buf[prefix_len] = '0'
return prefix_len + 1
}
i := prefix_len
loop if v <= 0 break else {
remainder := v % radix
v /= radix
if remainder > 9 {
buf[i] = @int_cast(remainder - 10 + 'A')
} else {
buf[i] = @int_cast(remainder + '0')
}
i += 1
}
_ = mem.reverse(buf[prefix_len..i])
return i
}
fmt_bool := fn(buf: []u8, v: bool): uint {
if v {
mem.copy(buf, "true")
return 4
} else {
mem.copy(buf, "false")
return 5
}
}
fmt_optional := fn(buf: []u8, v: @Any()): uint {
if v != null return format(buf, @as(@ChildOf(@TypeOf(v)), v.?))
mem.copy(buf, "null")
return 4
}
// todo: cleanup
fmt_enum := fn(buf: []u8, v: @Any()): uint {
T := @TypeOf(v)
len := @name_of(T).len
mem.copy(buf, @name_of(T))
mem.copy(buf[len..], ".(")
len += 2
len += fmt_int(buf[len..], @as(Type(T).USize(), @bit_cast(v)), 10)
mem.copy(buf[len..], ")")
return len + 1
}
format := fn(buf: []u8, v: @Any()): uint {
// $T := TypeOf(v)
T := Type(@TypeOf(v))
$match T.kind() {
.Pointer => return fmt_int(buf, @as(uint, @bit_cast(v)), 16),
.Builtin => {
$if T.is_int() return fmt_int(buf, v, 10)
$if T.is_bool() return fmt_bool(buf, v)
$if T.is_float() @error("todo: fmt_float")
},
.Struct => @error("todo: fmt_container"),
.Tuple => @error("todo: fmt_container"),
.Slice => @error("todo: fmt_container"),
.Array => @error("todo: fmt_container"),
.Optional => return fmt_optional(buf, v),
.Enum => return fmt_enum(buf, v),
_ => @error("formatting ", @TypeOf(v), " is not supported"),
}
}

View file

@ -1,86 +0,0 @@
lily.{target, mem, Type} := @use("lib.hb")
Buffer := struct {
.id: uint
Self := @CurrentScope()
new := fn(name: ?[]u8): ?Self {
id: uint = 0
if name == null {
id = target.buf_create()
} else {
id = target.buf_create_named(name.?)
}
if id == 0 return null
return Self.from_raw(id)
}
search := fn(name: []u8): ?Self {
id := target.buf_search(name)
if id == 0 return null
return Self.from_raw(id)
}
deinit := fn(self: ^Self): void {
target.buf_destroy(self.id)
self.* = idk
}
$from_raw := fn(id: uint): Self {
return .(id)
}
$await := fn(self: ^Self): void {
target.buf_await(self.id)
}
$write := fn(self: ^Self, val: @Any()): void {
$match Type(@TypeOf(val)).kind() {
.Pointer => target.buf_write(self.id, mem.as_bytes(val)),
.Slice => target.buf_write(self.id, mem.as_bytes(val)),
_ => target.buf_write(self.id, mem.as_bytes(&val)),
}
}
$read_into := fn(self: ^Self, slice: []u8): void {
target.buf_read(self.id, slice)
}
$read := fn(self: ^Self, $T: type): T {
buf: T = idk
target.buf_read(self.id, mem.as_bytes(&buf))
return buf
}
}
Channel := struct {
.local: Buffer;
.remote: Buffer
Self := @CurrentScope()
new := fn(local_name: ?[]u8, remote_name: ?[]u8): ?Self {
local := Buffer.new(local_name)
if local == null return null
remote := Buffer.new(remote_name)
if remote == null return null
return .(local.?, remote.?)
}
$deinit := fn(self: ^Self): void {
self.local.deinit()
self.remote.deinit()
self.* = idk
}
$to_remote := fn(self: Self): Self {
return .(self.remote, self.local)
}
$from_raw := fn(local_id: uint, remote_id: uint): Self {
return .(Buffer.from_raw(local_id), Buffer.from_raw(remote_id))
}
$await := fn(self: ^Self): void {
self.local.await()
}
$write := fn(self: ^Self, val: @Any()): void {
self.remote.write(val)
}
$read_into := fn(self: ^Self, slice: []u8): void {
self.local.read_into(slice)
}
$read := fn(self: ^Self, $T: type): T {
return self.local.read(T)
}
}

View file

@ -1,172 +0,0 @@
lily.{Type, alloc: .{Vec}} := @use("lib.hb")
Next := fn(T: type): type return struct {
.finished: bool;
.val: T
$yield := fn(val: T): @CurrentScope() return .(false, val)
$done := fn(): @CurrentScope() return .(true, idk)
}
Iterator := fn(T: type): type return struct {
.inner: T
IterNext := @TypeOf(T.next(idk))
IterVal := @TypeOf(T.next(idk).val)
Self := @CurrentScope()
$next := fn(self: ^Self): IterNext {
return self.inner.next()
}
$map := fn(self: ^Self, $func: type): Iterator(Map(T, func)) {
return .(.(self))
}
$enumerate := fn(self: ^Self): Iterator(Enumerate(T)) {
return .(.(self, 0))
}
$take := fn(self: ^Self, end: uint): Iterator(Take(T)) {
return .(.(self, 0, end))
}
$skip := fn(self: ^Self, n: uint): Iterator(Skip(T)) {
return .(.(self, n))
}
$chain := fn(self: ^Self, rhs: @Any()): Iterator(Chain(T, @TypeOf(rhs))) {
return .(.(self, rhs, .Iter0))
}
for_each := fn(self: ^Self, $func: type): void {
loop {
x := self.next()
if x.finished break
_ = func(x.val)
}
}
fold := fn(self: ^Self, $func: type, sum: @Any()): @TypeOf(sum) {
loop {
x := self.next()
if x.finished return sum
sum = func(sum, x.val)
}
}
nth := fn(self: ^Self, n: uint): ?IterVal {
i := 0
loop {
defer i += 1
x := self.next()
if x.finished return null else {
if i == n return x.val
}
}
}
collect := fn(self: ^Self, $A: type): ?A {
$if Type(A).kind() != .Array {
@error("collecting", Self, "into type", A, "unsupported for now")
}
$if @ChildOf(A) != IterVal {
@error("cannot collect of iterator of type", IterVal, "into type", A)
}
cont: A = idk
i := 0
loop {
defer i += 1
x := self.next()
if i == @len_of(A) & x.finished return cont
if i == @len_of(A) | x.finished return null
cont[i] = x.val
}
}
// ! broken
collect_vec := fn(self: ^Self, allocator: @Any()): Vec(IterVal, @ChildOf(@TypeOf(allocator))) {
vec := Vec(IterVal, @ChildOf(@TypeOf(allocator))).new(allocator)
loop {
x := self.next()
if x.finished return vec
vec.push(x.val)
}
}
}
Map := fn(T: type, func: type): type return struct {
.iter: ^Iterator(T)
IterNext := @TypeOf(func(idk))
$next := fn(self: ^@CurrentScope()): Next(IterNext) {
x := self.iter.inner.next()
return .(x.finished, func(x.val))
}
}
Enumerate := fn($T: type): type return struct {
.iter: ^Iterator(T);
.n: uint
IterNext := struct{.n: uint; .val: @TypeOf(T.next(idk).val)}
$next := fn(self: ^@CurrentScope()): Next(IterNext) {
self.n += 1
x := self.iter.inner.next()
return .(x.finished, .(self.n, x.val))
}
}
Take := fn($T: type): type return struct {
.iter: ^Iterator(T);
.n: uint;
.end: uint
IterNext := @TypeOf(T.next(idk).val)
$next := fn(self: ^@CurrentScope()): Next(IterNext) {
self.n += 1
x: Next(IterNext) = idk
if self.n > self.end return .(true, x.val)
return self.iter.inner.next()
}
}
Skip := fn($T: type): type return struct {
.iter: ^Iterator(T);
.step: uint
IterNext := @TypeOf(T.next(idk).val)
$next := fn(self: ^@CurrentScope()): Next(IterNext) {
n := 0
loop {
x := self.iter.next()
if n == self.step | x.finished return x
n += 1
}
}
}
Chain := fn($A: type, $B: type): type {
Iter0Next := @TypeOf(A.next(idk).val)
Iter1Next := @TypeOf(B.next(idk).val)
$if Iter0Next != Iter1Next @error(Iter0Next, " != ", Iter1Next)
return struct {
.iter0: ^Iterator(A)
/* todo: ^B? */;
.iter1: B;
.state: enum{.Iter0; .Iter0Finished; .BothFinished}
next := fn(self: ^@CurrentScope()): Next(Iter0Next) {
// todo: replace with Type(T).uninit()
x: Next(Iter0Next) = idk
match self.state {
.Iter0 => {
x = self.iter0.inner.next()
if x.finished {
self.state = .Iter0Finished
return self.next()
}
},
.Iter0Finished => {
x = self.iter1.inner.next()
if x.finished self.state = .BothFinished
},
_ => {
},
}
return .(self.state == .BothFinished, x.val)
}
}
}

View file

@ -1,29 +0,0 @@
.{Type, TypeOf} := @use("type.hb")
process := @use("process.hb")
target := @use("target/lib.hb")
alloc := @use("alloc/lib.hb")
iter := @use("iter.hb")
ipc := @use("ipc.hb")
mem := @use("mem.hb")
log := @use("log.hb")
fmt := @use("fmt.hb")
config := struct {
$DEBUG := true
$MIN_LOGLEVEL := log.LogLevel.Info
// sufficent for now.
$FMT_BUFFER_SIZE := 256
$min_loglevel := fn(): log.LogLevel {
$if config.DEBUG & config.MIN_LOGLEVEL < .Debug return .Debug
return config.MIN_LOGLEVEL
}
}
Version := struct {
.major: uint;
.minor: uint;
.patch: uint;
}
$VERSION := Version.(0, 1, 0)

1
src/liblily/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
build/*

13
src/liblily/Makefile Normal file
View file

@ -0,0 +1,13 @@
.PHONY: clean
build/liblily.a: build build/lily.o
ar rcs build/liblily.a build/lily.o
build/lily.o: build lily.c lily.h
gcc -I. -lc -c lily.c -o ./build/lily.o
build:
mkdir build
clean:
rm -rf build/

35
src/liblily/lily.c Normal file
View file

@ -0,0 +1,35 @@
#define _GNU_SOURCE 1
#include <sys/mman.h>
#include <string.h>
#include "lily.h"
void* lily_alloc(uint size){
return mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_ANONYMOUS, -1, 0);
}
void* lily_alloc_zeroed(uint size){
return lily_alloc(size);
}
void lily_dealloc(void* ptr, uint size){
munmap(ptr, size);
}
void* lily_realloc(void* ptr, uint size, uint size_new){
return mremap(ptr, size, size_new, MREMAP_MAYMOVE);
}
void lily_memset(void* dest, void* src, uint count, uint size){
uint total_size = count * size;
memcpy(dest, src, size);
uint copied = size;
while (copied < total_size) {
uint copy_size;
if(copied > total_size - copied)
copy_size = total_size - copied;
else
copy_size = copied;
memcpy(dest, (dest + copied), copy_size);
copied += copy_size;
}
}

14
src/liblily/lily.h Normal file
View file

@ -0,0 +1,14 @@
#ifndef __LILY_H_DEFINED
#define __LILY_H_DEFINED
#define uint unsigned long long int
void* lily_alloc(uint size);
void* lily_alloc_zeroed(uint size);
void lily_dealloc(void* ptr, uint size);
void* lily_realloc(void* ptr, uint size, uint size_new);
void lily_memset(void* dest, void* src, uint count, uint size);
void lily_getrandom(void* dest, uint size);
#endif // __LILY_H_DEFINED

73
src/lily/alloc/arena.hb Normal file
View file

@ -0,0 +1,73 @@
.{Config, Target, Type, log, collections: .{Vec}, alloc: .{RawAllocator}} := @use("../lib.hb");
Allocation := struct {
ptr: ^u8,
len: uint,
}
ArenaAllocator := struct {
ptr: ^u8,
size: uint,
allocated: uint,
allocations: Vec(Allocation, RawAllocator),
raw: RawAllocator,
new := fn(): Self {
size := Target.page_size()
// todo(?): spec should accept ?Self as return type
ptr := @unwrap(Target.alloc_zeroed(size))
raw := RawAllocator.new()
vec := Vec(Allocation, RawAllocator).new(&raw)
return .(ptr, size, 0, vec, raw)
}
deinit := fn(self: ^Self): void {
Target.dealloc(self.ptr, self.size)
self.allocations.deinit()
self.raw.deinit()
log.debug("deinit: allocator")
}
alloc := fn(self: ^Self, $T: type, count: uint): ?^T {
if self.allocated + count * @sizeof(T) > self.size {
// ! (libc) (compiler) bug: null check broken. unwrapping.
self.ptr = @unwrap(Target.realloc(self.ptr, self.size, self.size * 2))
self.size = self.size * 2
}
allocation := self.ptr + self.allocated
self.allocations.push(.(allocation, count * @sizeof(T)))
self.allocated = self.allocated + count * @sizeof(T)
log.debug("allocated")
return @bitcast(allocation)
}
$alloc_zeroed := fn(self: ^Self, $T: type, count: uint): ?^T {
return self.alloc(T, count)
}
realloc := fn(self: ^Self, $T: type, ptr: ^T, count: uint): ?^T {
old_size := self._find_size(ptr)
if old_size == null return null
if old_size > @sizeof(T) * count {
if Config.debug_assertions() {
log.warn("arena allocator: new_size is smaller than old_size")
}
return ptr
}
new_ptr := @unwrap(self.alloc(T, count))
_ = Target.memcpy(new_ptr, ptr, old_size)
return new_ptr
}
dealloc := fn(self: ^Self, $T: type, ptr: ^T): void {
log.debug("freed")
}
_find_size := fn(self: ^Self, ptr: ^u8): ?uint {
i := 0
loop if i == self.allocations.len() break else {
defer i += 1
alloced := self.allocations.get_unchecked(i)
return alloced.len
}
return null
}
}

3
src/lily/alloc/lib.hb Normal file
View file

@ -0,0 +1,3 @@
.{RawAllocator} := @use("raw.hb");
.{SimpleAllocator} := @use("simple.hb");
.{ArenaAllocator} := @use("arena.hb");

45
src/lily/alloc/raw.hb Normal file
View file

@ -0,0 +1,45 @@
.{Config, Target, Type, log, collections: .{Vec}} := @use("../lib.hb");
RawAllocator := struct {
ptr: ^u8,
size: uint,
$new := fn(): Self return .(Type(^u8).uninit(), 0)
deinit := fn(self: ^Self): void {
self.dealloc(u8, Type(^u8).uninit());
*self = Self.new()
log.debug("deinit: raw allocator")
}
alloc := fn(self: ^Self, $T: type, count: uint): ?^T {
// ! (libc) (compiler) bug: null check broken. unwrapping.
ptr := @unwrap(Target.alloc(count * @sizeof(T)))
self.ptr = ptr
self.size = count * @sizeof(T)
log.debug("allocated raw")
return @bitcast(ptr)
}
alloc_zeroed := fn(self: ^Self, $T: type, count: uint): ?^T {
// ! (libc) (compiler) bug: null check broken. unwrapping.
ptr := @unwrap(Target.alloc_zeroed(count * @sizeof(T)))
self.ptr = ptr
self.size = count * @sizeof(T)
return @bitcast(ptr)
}
realloc := fn(self: ^Self, $T: type, ptr: ^T, count: uint): ?^T {
if self.size == 0 return null
log.debug("reallocated raw")
// ! (libc) (compiler) bug: null check broken. unwrapping.
new_ptr := @unwrap(Target.realloc(self.ptr, self.size, count * @sizeof(T)))
self.ptr = new_ptr
self.size = count * @sizeof(T)
return @bitcast(new_ptr)
}
// ! INLINING THIS FUNCTION CAUSES MISCOMPILATION!! DO NOT INLINE IT!! :) :) :)
dealloc := fn(self: ^Self, $T: type, ptr: ^T): void {
if self.size == 0 return;
Target.dealloc(self.ptr, self.size)
log.debug("freed raw")
}
}

91
src/lily/alloc/simple.hb Normal file
View file

@ -0,0 +1,91 @@
.{Config, Target, Type, log, collections: .{Vec}} := @use("../lib.hb");
.{RawAllocator} := @use("lib.hb")
Allocation := struct {
ptr: ^u8,
len: uint,
}
// ! THIS ALLOCATOR IS TEMPORARY
SimpleAllocator := struct {
allocations: Vec(Allocation, RawAllocator),
raw: RawAllocator,
$new := fn(): Self {
raw := RawAllocator.new()
return .(Vec(Allocation, RawAllocator).new(&raw), raw)
}
deinit := fn(self: ^Self): void {
loop if self.allocations.len() == 0 break else {
alloced := self.allocations.pop_unchecked()
match Target.current() {
.LibC => Target.dealloc(alloced.ptr),
.AbleOS => Target.dealloc(alloced.ptr, alloced.len),
}
}
self.allocations.deinit()
self.raw.deinit()
log.debug("deinit: allocator")
}
alloc := fn(self: ^Self, $T: type, count: uint): ?^T {
ptr := Target.alloc(count * @sizeof(T))
if Target.current() == .AbleOS {
if ptr != null self.allocations.push(.(ptr, count * @sizeof(T)))
}
log.debug("allocated")
return @bitcast(ptr)
}
alloc_zeroed := fn(self: ^Self, $T: type, count: uint): ?^T {
ptr := Target.alloc_zeroed(count * @sizeof(T))
if Target.current() == .AbleOS {
if ptr != null self.allocations.push(.(ptr, count * @sizeof(T)))
}
return @bitcast(ptr)
}
realloc := fn(self: ^Self, $T: type, ptr: ^T, count: uint): ?^T {
match Target.current() {
.AbleOS => {
alloced := self._find_and_remove(@bitcast(ptr))
if alloced == null return null
new_ptr := Target.realloc(@bitcast(ptr), alloced.len, count * @sizeof(T))
if new_ptr != null {
self.allocations.push(.(new_ptr, count * @sizeof(T)))
} else {
self.allocations.push(alloced)
}
return @bitcast(new_ptr)
},
.LibC => {
new_ptr := Target.realloc(@bitcast(ptr), count * @sizeof(T))
log.debug("reallocated")
return @bitcast(new_ptr)
},
}
}
dealloc := fn(self: ^Self, $T: type, ptr: ^T): void {
match Target.current() {
.AbleOS => {
alloced := self._find_and_remove(@bitcast(ptr))
if alloced != null Target.dealloc(@bitcast(ptr), alloced.len)
},
.LibC => {
Target.dealloc(@bitcast(ptr))
},
}
log.debug("freed")
}
_find_and_remove := fn(self: ^Self, ptr: ^u8): ?Allocation {
i := 0
loop if i == self.allocations.len() break else {
defer i += 1
alloced := self.allocations.get_unchecked(i)
if alloced.ptr == ptr {
_ = self.allocations.swap_remove(i)
return alloced
}
}
return null
}
}

BIN
src/lily/assets/zeroed Normal file

Binary file not shown.

View file

@ -0,0 +1,245 @@
.{collections: .{Vec}, iter: .{Iterator, IterNext}, Type, TypeOf, log} := @use("../lib.hb")
Item := fn($Key: type, $Value: type): type return packed struct {
key: Key,
value: Value,
}
Bucket := fn($Key: type, $Value: type, $Allocator: type): type {
return Vec(Item(Key, Value), Allocator)
}
Buckets := fn($Key: type, $Value: type, $Allocator: type): type {
return Vec(Bucket(Key, Value, Allocator), Allocator)
}
$equals := fn(lhs: @Any(), rhs: @TypeOf(lhs)): bool {
match TypeOf(lhs).kind() {
.Slice => return lhs.ptr == rhs.ptr & lhs.len == rhs.len,
_ => return lhs == rhs,
}
}
// temporarily here.
$next_power_of_two := fn(n: uint): uint {
n -= 1
n |= n >> 1
n |= n >> 2
n |= n >> 4
n |= n >> 8
n |= n >> 16
n |= n >> 32
return n + 1
}
HashMap := fn($Key: type, $Value: type, $Hasher: type, $Allocator: type): type return struct {
allocator: ^Allocator,
hasher: Hasher,
buckets: Buckets(Key, Value, Allocator),
length: uint,
new := fn(allocator: ^Allocator): Self {
hasher := Hasher.default()
buckets := Buckets(Key, Value, Allocator).with_capacity(allocator, 16)
// ! (compiler) bug: have to use for-loop here rather than using buckets.len(), otherwise we loop infinitely
i := 0
loop if i == 16 break else {
defer i += 1
buckets.push(Bucket(Key, Value, Allocator).new(allocator))
}
// also need to add this here...?
buckets.slice.len = 16
return .(allocator, hasher, buckets, 0)
}
// seems like bad performance...
resize := fn(self: ^Self): void {
new_cap := next_power_of_two(self.buckets.len() * 2)
new_buckets := @TypeOf(self.buckets).with_capacity(self.allocator, new_cap)
// same compiler bug as above...
i := 0
loop if i == new_cap break else {
defer i += 1
new_buckets.push(Bucket(Key, Value, Allocator).new(self.allocator))
}
new_buckets.slice.len = new_cap
loop if self.buckets.len() == 0 break else {
bucket := self.buckets.pop_unchecked()
loop if bucket.len() == 0 break else {
item := bucket.pop_unchecked()
self.hasher.write(item.key)
idx := self.hasher.finish() & new_cap - 1
self.hasher.reset()
new_bucket := new_buckets.get_ref_unchecked(idx)
new_bucket.push(item)
}
bucket.deinit()
}
self.buckets.deinit()
self.buckets = new_buckets
}
deinit := fn(self: ^Self): void {
loop {
bucket := self.buckets.pop()
if bucket == null break;
bucket.deinit()
}
self.buckets.deinit()
self.hasher.deinit()
self.length = 0
}
insert := fn(self: ^Self, key: Key, value: Value): ^Value {
self.hasher.write(key)
idx := self.hasher.finish() & self.buckets.len() - 1
self.hasher.reset()
if self.length * 4 > self.buckets.len() * 3 {
@inline(self.resize)
}
bucket_opt := self.buckets.get_ref(idx)
if bucket_opt == null {
self.buckets.push(Bucket(Key, Value, Allocator).new(self.allocator))
bucket_opt = self.buckets.get_ref(self.buckets.len() - 1)
}
bucket := @unwrap(bucket_opt)
i := 0
loop if i == bucket.len() break else {
defer i += 1
pair := bucket.get_ref_unchecked(i)
if equals(pair.key, key) {
pair.value = value
// ! weird no-op cast to stop type system from complaining.
// don't quite know what is going on here...
return &@as(^Item(Key, Value), pair).value
}
}
bucket.push(.{key, value})
pair := bucket.get_ref_unchecked(bucket.len() - 1)
self.length += 1
return &@as(^Item(Key, Value), pair).value
}
get := fn(self: ^Self, key: Key): ?Value {
self.hasher.write(key)
idx := self.hasher.finish() & self.buckets.len() - 1
self.hasher.reset()
bucket := self.buckets.get_ref(idx)
if bucket == null return null
i := 0
loop if i == bucket.len() break else {
defer i += 1
pair := bucket.get_ref_unchecked(i)
if equals(pair.key, key) {
return pair.value
}
}
return null
}
// references may be invalidated if value is removed from hashmap after get_ref is used.
get_ref := fn(self: ^Self, key: Key): ?^Value {
self.hasher.write(key)
idx := self.hasher.finish() & self.buckets.len() - 1
self.hasher.reset()
bucket := self.buckets.get_ref(idx)
if bucket == null return null
i := 0
loop if i == bucket.len() break else {
defer i += 1
pair := bucket.get_ref_unchecked(i)
if equals(pair.key, key) {
return &@as(^Item(Key, Value), pair).value
}
}
return null
}
remove := fn(self: ^Self, key: Key): ?Value {
self.hasher.write(key)
idx := self.hasher.finish() & self.buckets.len() - 1
self.hasher.reset()
bucket := self.buckets.get_ref(idx)
if bucket == null return null
i := 0
loop if i == bucket.len() break else {
defer i += 1
pair := bucket.get_ref_unchecked(i)
if equals(pair.key, key) {
self.length -= 1
return @unwrap(bucket.swap_remove(i)).value
}
}
return null
}
// todo: write keys, values
$items := fn(self: Self): Iterator(Items(Self, Item(Key, Value))) {
return .(.(self, 0, 0))
}
$keys := fn(self: Self): Iterator(Keys(Self, Key)) {
return .(.(self, 0, 0))
}
$values := fn(self: Self): Iterator(Values(Self, Value)) {
return .(.(self, 0, 0))
}
$len := fn(self: ^Self): uint return self.length
}
// todo: make these efficient and reduce code duplication
Items := fn($H: type, $I: type): type return struct {
// has to be owned here... (possibly due to bug) great...
map: H,
bucket: uint,
sub: uint,
next := fn(self: ^Self): IterNext(I) {
bucket := self.map.buckets.get_ref(self.bucket)
if bucket == null return .(true, Type(I).uninit())
sub := bucket.get(self.sub)
if sub == null {
self.sub = 0
self.bucket += 1
return self.next()
}
self.sub += 1
return .(false, sub)
}
}
Values := fn($H: type, $V: type): type return struct {
// has to be owned here... (possibly due to bug) great...
map: H,
bucket: uint,
sub: uint,
next := fn(self: ^Self): IterNext(V) {
bucket := self.map.buckets.get_ref(self.bucket)
if bucket == null return .(true, Type(V).uninit())
sub := bucket.get(self.sub)
if sub == null {
self.sub = 0
self.bucket += 1
return self.next()
}
self.sub += 1
return .(false, sub.value)
}
}
Keys := fn($H: type, $K: type): type return struct {
// has to be owned here... (possibly due to bug) great...
map: H,
bucket: uint,
sub: uint,
next := fn(self: ^Self): IterNext(K) {
bucket := self.map.buckets.get_ref(self.bucket)
if bucket == null return .(true, Type(K).uninit())
sub := bucket.get(self.sub)
if sub == null {
self.sub = 0
self.bucket += 1
return self.next()
}
self.sub += 1
return .(false, sub.key)
}
}

View file

@ -0,0 +1,2 @@
.{Vec} := @use("vec.hb");
.{HashMap} := @use("hashmap.hb")

View file

@ -0,0 +1,95 @@
.{memmove, Type, log, alloc, quicksort, compare, iter} := @use("../lib.hb");
Vec := fn($T: type, $Allocator: type): type return struct {
slice: []T,
allocator: ^Allocator,
cap: uint = 0,
$new := fn(allocator: ^Allocator): Self return .{slice: Type([]T).uninit(), allocator}
$with_capacity := fn(allocator: ^Allocator, cap: uint): Self {
// ! (libc) (compiler) bug: null check broken, so unwrapping (unsafe!)
new_alloc := @unwrap(allocator.alloc(T, cap))
return .{slice: new_alloc[0..0], allocator, cap}
}
deinit := fn(self: ^Self): void {
// currently does not handle deinit of T if T allocates memory
if self.cap > 0 self.allocator.dealloc(T, self.slice.ptr)
self.slice = Type([]T).uninit()
self.cap = 0
if Allocator == alloc.RawAllocator {
log.debug("deinit: vec (w/ raw allocator)")
} else {
log.debug("deinit: vec")
}
}
// todo: maybe make this exponential instead
reserve := fn(self: ^Self, n: uint): void {
if self.len() + n <= self.cap return;
// ! (libc) (compiler) bug: null check broken, so unwrapping (unsafe!)
new_alloc := @unwrap(self.allocator.realloc(T, self.slice.ptr, self.cap + n))
self.cap += n
self.slice.ptr = new_alloc
}
push := fn(self: ^Self, value: T): void {
if self.slice.len == self.cap {
if self.cap == 0 {
// ! (libc) (compiler) bug: null check broken, so unwrapping (unsafe!)
new_alloc := @unwrap(self.allocator.alloc(T, self.cap))
self.slice.ptr = new_alloc
self.cap = 1
} else {
self.cap *= 2
// ! (libc) (compiler) bug: null check broken, so unwrapping (unsafe!)
new_alloc := @unwrap(self.allocator.realloc(T, self.slice.ptr, self.cap))
self.slice.ptr = new_alloc
}
}
self.slice[self.slice.len] = value
self.slice.len += 1
}
get := fn(self: ^Self, n: uint): ?T {
if n >= self.slice.len return null
return self.slice[n]
}
$get_unchecked := fn(self: ^Self, n: uint): T return self.slice[n]
get_ref := fn(self: ^Self, n: uint): ?^T {
if n >= self.slice.len return null
return self.slice.ptr + n
}
$get_ref_unchecked := fn(self: ^Self, n: uint): ^T return self.slice.ptr + n
pop := fn(self: ^Self): ?T {
if self.slice.len == 0 return null
self.slice.len -= 1
// as far as im aware this is not undefined behaviour.
return self.slice[self.slice.len]
}
$pop_unchecked := fn(self: ^Self): T {
self.slice.len -= 1
// as far as im aware this is not undefined behaviour. (2)
return self.slice[self.slice.len]
}
remove := fn(self: ^Self, n: uint): ?T {
if n >= self.slice.len return null
if n + 1 == self.slice.len return self.pop_unchecked()
temp := self.slice[n]
memmove(self.slice.ptr + n, self.slice.ptr + n + 1, (self.slice.len - n - 1) * @sizeof(T))
self.slice.len -= 1
return temp
}
swap_remove := fn(self: ^Self, n: uint): ?T {
if n >= self.slice.len return null
if n + 1 == self.slice.len return self.pop_unchecked()
temp := self.slice[n]
self.slice[n] = self.pop_unchecked()
return temp
}
find := fn(self: ^Self, rhs: T): ?uint {
i := 0
loop if self.get_unchecked(i) == rhs return i else if i == self.slice.len return null else i += 1
}
$sort_with := fn(self: ^Self, $func: type): void {
_ = quicksort(func, self.slice, 0, self.slice.len - 1)
}
$sort := fn(self: ^Self): void self.sort_with(compare)
$len := fn(self: ^Self): uint return self.slice.len
$capacity := fn(self: ^Self): uint return self.cap
}

274
src/lily/fmt.hb Normal file
View file

@ -0,0 +1,274 @@
.{target, Type, TypeOf, string, memcpy, panic} := @use("lib.hb")
$FP_TOLERANCE := 0.00000001
// ! (libc) (compiler) bug: caused by: `lily.log.print(100)`
fmt_int := fn(buf: []u8, v: @Any(), radix: @TypeOf(v)): uint {
prefix_len := 0
// ! (compiler) bug: excuse me wtf? why are `v > 0` and `v < 0` flipped?
if TypeOf(v).is_signed_int() & v > 0 {
v = -v
buf[0] = '-'
prefix_len += 1
}
if radix == 16 {
memcpy(buf.ptr + prefix_len, "0x".ptr, 2)
prefix_len += 2
} else if radix == 2 {
memcpy(buf.ptr + prefix_len, "0b".ptr, 2)
prefix_len += 2
} else if radix == 8 {
memcpy(buf.ptr + prefix_len, "0o".ptr, 2)
prefix_len += 2
}
if v == 0 {
buf[prefix_len] = '0'
return prefix_len + 1
}
i := prefix_len
loop if v <= 0 break else {
remainder := v % radix
v /= radix
// ! (libc) workaround for compiler bug
// if remainder > 9 {
// buf[i] = @intcast(remainder - 10 + 'A')
// } else {
// buf[i] = @intcast(remainder + '0')
// }
if remainder > 9 {
remainder += 'A' - 10
} else {
remainder += '0'
}
buf[i] = @intcast(remainder)
i += 1
}
string.reverse(buf[prefix_len..i])
return i
}
// ! (libc) (compiler) keeps complaining about 'not yet implemented' only on libc
fmt_float := fn(buf: []u8, v: @Any(), precision: uint, radix: int): uint {
prefix_len := 0
if v < 0 {
v = -v
buf[0] = '-'
prefix_len += 1
}
if radix == 16 {
memcpy(buf.ptr + prefix_len, "0x".ptr, 2)
prefix_len += 2
} else if radix == 2 {
memcpy(buf.ptr + prefix_len, "0b".ptr, 2)
prefix_len += 2
} else if radix == 8 {
memcpy(buf.ptr + prefix_len, "0o".ptr, 2)
prefix_len += 2
}
integer_part := @fti(v)
fractional_part := v - @itf(integer_part)
i := prefix_len
loop if integer_part == 0 & i > prefix_len break else {
remainder := integer_part % radix
integer_part /= radix
if remainder > 9 {
buf[i] = @intcast(remainder - 10 + 'A')
} else {
buf[i] = @intcast(remainder + '0')
}
i += 1
}
string.reverse(buf[prefix_len..i])
if fractional_part > FP_TOLERANCE {
buf[i] = '.'
i += 1
p := precision
loop if p <= 0 | fractional_part < FP_TOLERANCE break else {
fractional_part *= @itf(radix)
digit := @fti(fractional_part)
if digit > 9 {
buf[i] = @intcast(digit - 10 + 'A')
} else {
buf[i] = @intcast(digit + '0')
}
i += 1
fractional_part -= @itf(digit)
p -= 1
}
}
return i
}
fmt_bool := fn(buf: []u8, v: bool): uint {
if v {
memcpy(buf.ptr, "true".ptr, 4)
return 4
} else {
memcpy(buf.ptr, "false".ptr, 5)
return 5
}
}
fmt_container := fn(buf: []u8, v: @Any()): uint {
T := TypeOf(v)
i := 0
len := 0
if T.kind() == .Struct {
memcpy(buf.ptr + len, T.name().ptr, T.name().len)
len += T.name().len
memcpy(buf.ptr + len, ".(".ptr, 2)
len += 2
} else if T.kind() == .Slice | T.kind() == .Array {
memcpy(buf.ptr + len, T.Child().name().ptr, T.Child().name().len)
len += T.Child().name().len
memcpy(buf.ptr + len, ".[".ptr, 2)
len += 2
} else if T.kind() == .Tuple {
memcpy(buf.ptr + len, ".(".ptr, 2)
len += 2
}
if T.kind() == .Slice {
loop if i == v.len break else {
len += format(buf[len..], v[i])
i += 1
if i < v.len {
memcpy(buf.ptr + len, ", ".ptr, 2)
len += 2
}
}
} else {
$loop if i == T.len() break else {
len += format(buf[len..], v[i])
i += 1
if i < T.len() {
memcpy(buf.ptr + len, ", ".ptr, 2)
len += 2
}
}
}
if T.kind() == .Struct | T.kind() == .Tuple {
*(buf.ptr + len) = ')'
len += 1
} else if T.kind() == .Slice | T.kind() == .Array {
*(buf.ptr + len) = ']'
len += 1
}
return len
}
fmt_optional := fn(buf: []u8, v: @Any()): uint {
if v != null return format(buf, @as(@ChildOf(@TypeOf(v)), v))
memcpy(buf.ptr, @nameof(@TypeOf(v)).ptr, @nameof(@TypeOf(v)).len)
memcpy(buf.ptr + @nameof(@TypeOf(v)).len, ".null".ptr, 5)
return @nameof(@TypeOf(v)).len + 5
}
// todo: clean up this and other functions
fmt_enum := fn(buf: []u8, v: @Any()): uint {
T := @TypeOf(v)
len := @nameof(T).len;
memcpy(buf.ptr, @nameof(T).ptr, len)
memcpy(buf.ptr + len, ".(".ptr, 2)
len += 2
len += fmt_int(buf[len..], @as(Type(T).USize(), @bitcast(v)), 10);
memcpy(buf.ptr + len, ")".ptr, 1)
return len + 1
}
format := fn(buf: []u8, v: @Any()): uint {
T := TypeOf(v)
match T.kind() {
.Pointer => return fmt_int(buf, @as(uint, @bitcast(v)), 16),
.Builtin => {
if T.is_int() return fmt_int(buf, v, 10)
if T.is_bool() return fmt_bool(buf, v)
if T.is_float() return fmt_float(buf, v, 1 << 32, 10)
},
.Struct => return fmt_container(buf, v),
.Tuple => return fmt_container(buf, v),
.Slice => {
if T.This() == []u8 {
*buf.ptr = '"'
memcpy(buf.ptr + 1, v.ptr, v.len);
*(buf.ptr + 1 + v.len) = '"'
return v.len + 2
}
return fmt_container(buf, v)
},
.Array => return fmt_container(buf, v),
.Optional => return fmt_optional(buf, v),
.Enum => return fmt_enum(buf, v),
_ => @error("format(", T, ") is not supported."),
}
return 0
}
// ! (compiler) bug: panic doesnt work here specifically. causes parser issue.
format_with_str := fn(str: []u8, buf: []u8, v: @Any()): uint {
T := TypeOf(v)
n := string.count(str, '{')
// if n != string.count(str, '}') panic("Missing closing '}' in format string.")
if n != string.count(str, '}') die
if T.kind() == .Tuple {
// if T.len() != n panic("Format string has different number of '{}' than args given.")
if T.len() != n die
m := 0
i := 0
j := 0
$loop if m > T.len() break else {
if m == T.len() {
loop if i == str.len break else {
buf[j] = str[i]
i += 1
j += 1
}
m += 1
} else {
v2 := v[m]
loop if i == str.len break else {
if str[i] == '{' & str[i + 1] == '}' {
j += format(buf[j..], v2)
i += 2
break
} else {
buf[j] = str[i]
i += 1
j += 1
}
}
m += 1
}
}
return j
} else if n > 1 {
// panic("Format string has multiple '{}' but value provided is not a tuple.")
die
} else {
i := 0
j := 0
loop if i == str.len break else {
if str[i] == '{' & str[i + 1] == '}' {
j += format(buf[j..], v)
i += 2
} else {
buf[j] = str[i]
i += 1
j += 1
}
}
return j
}
}

209
src/lily/hash/foldhash.hb Normal file
View file

@ -0,0 +1,209 @@
/*
* This code is an implementation of the FoldHash algorithm from https://github.com/orlp/foldhash,
* originally written by Orson Peters under the zlib license.
*
* Changes to the original code were made to meet the simplicity requirements of this implementation.
* Behaviour aims to be equivalent but not identical to the original code.
*
* Copyright (c) 2024 Orson Peters
*
* This software is provided 'as-is', without any express or implied warranty. In
* no event will the authors be held liable for any damages arising from the use of
* this software.
*
* Permission is granted to anyone to use this software for any purpose, including
* commercial applications, and to alter it and redistribute it freely, subject to
* the following restrictions:
*
* 1. The origin of this software must not be misrepresented; you must not claim
* that you wrote the original software. If you use this software in a product,
* an acknowledgment in the product documentation would be appreciated but is
* not required.
*
* 2. Altered source versions must be plainly marked as such, and must not be
* misrepresented as being the original software.
*
* 3. This notice may not be removed or altered from any source distribution.
*/;
.{math, Target, TypeOf} := @use("../lib.hb")
$ARBITRARY0 := 0x243F6A8885A308D3
$ARBITRARY1 := 0x13198A2E03707344
$ARBITRARY2 := 0xA4093822299F31D0
$ARBITRARY3 := 0x82EFA98EC4E6C89
$ARBITRARY4 := 0x452821E638D01377
$ARBITRARY5 := 0xBE5466CF34E90C6C
$ARBITRARY6 := 0xC0AC29B7C97C50DD
$ARBITRARY7 := 0x3F84D5B5B5470917
$ARBITRARY8 := 0x9216D5D98979FB1B
$ARBITRARY9 := 0xD1310BA698DFB5AC
$FIXED_GLOBAL_SEED := uint.[ARBITRARY4, ARBITRARY5, ARBITRARY6, ARBITRARY7]
U128 := packed struct {a: uint, b: uint}
$folded_multiply := fn(x: uint, y: uint): uint {
lx: u32 = @intcast(x)
ly: u32 = @as(u32, @intcast(y))
hx := x >> 32
hy := y >> 32
afull := lx * hy
bfull := hx * ly
return afull ^ (bfull << 32 | bfull >> 32)
}
// ! (libc) (compiler) panics with "not yet implemented: bool" when using slice
hash_bytes_medium := fn(bytes: ^u8, len: uint, s0: uint, s1: uint, fold_seed: uint): uint {
lo := bytes
end := bytes + len
hi := end - 16
loop if lo >= hi return s0 ^ s1 else {
a := *@as(^uint, @bitcast(lo))
b := *@as(^uint, @bitcast(lo + 8))
c := *@as(^uint, @bitcast(hi))
d := *@as(^uint, @bitcast(hi + 8))
s0 = folded_multiply(a ^ s0, c ^ fold_seed)
s1 = folded_multiply(b ^ s1, d ^ fold_seed)
hi -= 16
lo += 16
}
return s0 ^ s1
}
hash_bytes_long := fn(bytes: ^u8, len: uint, s0: uint, s1: uint, s2: uint, s3: uint, fold_seed: uint): uint {
$chunk_size := 64
chunks := len / chunk_size
remainder := len % chunk_size
ptr := bytes
i := 0
loop if i >= chunks break else {
a := *@as(^uint, @bitcast(ptr))
b := *@as(^uint, @bitcast(ptr + 8))
c := *@as(^uint, @bitcast(ptr + 16))
d := *@as(^uint, @bitcast(ptr + 24))
e := *@as(^uint, @bitcast(ptr + 32))
f := *@as(^uint, @bitcast(ptr + 40))
g := *@as(^uint, @bitcast(ptr + 48))
h := *@as(^uint, @bitcast(ptr + 56))
s0 = folded_multiply(a ^ s0, e ^ fold_seed)
s1 = folded_multiply(b ^ s1, f ^ fold_seed)
s2 = folded_multiply(c ^ s2, g ^ fold_seed)
s3 = folded_multiply(d ^ s3, h ^ fold_seed)
ptr += chunk_size
i += 1
}
s0 ^= s2
s1 ^= s3
if remainder > 0 {
remainder_start := bytes + len - math.max(remainder, 16)
return hash_bytes_medium(remainder_start, math.max(remainder, 16), s0, s1, fold_seed)
}
return s0 ^ s1
}
FoldHasher := struct {
accumulator: uint,
original_seed: uint,
sponge: U128,
sponge_len: u8,
fold_seed: uint,
expand_seed: uint,
expand_seed2: uint,
expand_seed3: uint,
$new := fn(seed: uint): Self {
return .(
seed,
seed,
.(0, 0),
0,
FIXED_GLOBAL_SEED[0],
FIXED_GLOBAL_SEED[1],
FIXED_GLOBAL_SEED[2],
FIXED_GLOBAL_SEED[3],
)
}
$deinit := fn(self: ^Self): void {
}
$default := fn(): Self {
a := 0
Target.getrandom(@bitcast(&a), @sizeof(@TypeOf(a)))
return Self.new(a)
}
write := fn(self: ^Self, any: @Any()): void {
T := TypeOf(any)
// ! broken
// if T.is_int() {
// bits := 8 * T.size()
// if @as(uint, self.sponge_len) + bits > 128 {
// self.accumulator = folded_multiply(self.sponge.b ^ self.accumulator, self.sponge.a ^ self.fold_seed)
// self.sponge = *@bitcast(&any)
// self.sponge_len = @intcast(bits)
// } else {
// // note: this behaviour is incorrect with U128 struct
// self.sponge |= *@as(^U128, @bitcast(&any)) << *@as(^U128, @bitcast(&self.sponge_len))
// self.sponge_len += @intcast(bits)
// }
// return
// }
bytes := @as(^u8, @bitcast(&any))
len := 0
// ! (libc) (compiler) crashes when setting len = any.len
match T.kind() {
.Slice => {
len = any.len
bytes = any.ptr
},
_ => len = @sizeof(T.This()),
}
s0 := self.accumulator
s1 := self.expand_seed
if len <= 16 {
if len >= 8 {
s0 ^= *@bitcast(bytes)
s1 ^= *@bitcast(bytes + len - 8)
} else if len >= 4 {
s0 ^= *@as(^u32, @bitcast(bytes))
s1 ^= *@as(^u32, @bitcast(bytes + len - 4))
} else if len > 0 {
lo := *bytes
mid := *(bytes + len / 2)
hi := *(bytes + len - 1)
s0 ^= lo
s1 ^= @as(uint, hi) << 8 | mid
}
self.accumulator = folded_multiply(s0, s1)
} else if len < 256 {
self.accumulator = hash_bytes_medium(bytes, len, s0, s1, self.fold_seed)
} else {
self.accumulator = @inline(hash_bytes_long, bytes, len, s0, s1, self.expand_seed2, self.expand_seed3, self.fold_seed)
}
}
finish := fn(self: ^Self): uint {
if self.sponge_len > 0 {
return folded_multiply(self.sponge.b ^ self.accumulator, self.sponge.a ^ self.fold_seed)
} else {
return self.accumulator
}
}
reset := fn(self: ^Self): void {
self.accumulator = self.original_seed
self.sponge = .(0, 0)
self.sponge_len = 0
}
}

1
src/lily/hash/lib.hb Normal file
View file

@ -0,0 +1 @@
.{FoldHasher} := @use("foldhash.hb")

217
src/lily/iter.hb Normal file
View file

@ -0,0 +1,217 @@
.{Type} := @use("lib.hb")
IterNext := fn($T: type): type return struct {finished: bool, val: T}
// ! todo: complain about inlining rules
// ! how am i supposed to get optimal performance out of this if inlining is sometimes not allowed
// ! todo:
// * Iterator.peek
/// Iterator struct. Implements iterator stuff for you if you implement `into_iter` for your struct.
Iterator := fn($T: type): type {
$Next := @TypeOf(T.next(idk))
$Value := @TypeOf(T.next(idk).val)
return struct {
inner: T,
$next := fn(self: ^Self): Next {
return self.inner.next()
}
$map := fn(self: Self, $_map: type): Iterator(Map(T, _map)) {
return .(.(self))
}
$enumerate := fn(self: Self): Iterator(Enumerate(T)) {
return .(.{iter: self})
}
$take := fn(self: Self, n: uint): Iterator(Take(T)) {
return .(.{iter: self, end: n})
}
$skip := fn(self: Self, n: uint): Iterator(Skip(T)) {
return .(.{iter: self, step: n})
}
$chain := fn(self: Self, rhs: @Any()): Iterator(Chain(T, @TypeOf(rhs))) {
return .(.{iter0: self, iter1: .(rhs)})
}
$intersperse := fn(self: Self, rhs: @Any()): Iterator(Intersperse(T, @TypeOf(rhs))) {
return .(.{iter0: self, iter1: .(rhs)})
}
for_each := fn(self: ^Self, $_for_each: type): void {
loop {
x := self.next()
if x.finished break
_ = _for_each(x.val)
}
}
fold := fn(self: ^Self, $_fold: type, sum: @Any()): @TypeOf(sum) {
loop {
x := self.next()
if x.finished return sum
sum = _fold(sum, x.val)
}
}
nth := fn(self: ^Self, n: uint): ?Value {
i := 0
loop {
x := self.next()
if x.finished return null else if i == n return x.val
i += 1
}
}
collect := fn(self: ^Self, $A: type): ?A {
if Type(A).kind() != .Array {
@error("unsupported collect (for now)")
}
if @ChildOf(A) != Value {
@error("cannot collect of iterator of type", Value, "into type", A)
}
cont := Type(A).uninit()
i := 0
loop {
defer i += 1
x := self.next()
if i == @lenof(A) & x.finished return cont
if i == @lenof(A) | x.finished return null
cont[i] = x.val
}
}
}
}
/// Map is lazy. Simply calling `my_iter.map(func)` will not cause any execution.
Map := fn($T: type, $_map: type): type {
$Next := @TypeOf(_map(@as(@TypeOf(T.next(idk).val), idk)))
return struct {
iter: Iterator(T),
next := fn(self: ^Self): IterNext(Next) {
x := self.iter.inner.next()
return .(x.finished, _map(x.val))
}
}
}
IterEnumerate := fn($T: type): type return struct {n: uint, val: T}
Enumerate := fn($T: type): type {
$Next := IterEnumerate(@TypeOf(T.next(idk).val))
return struct {
iter: Iterator(T),
n: uint = 0,
next := fn(self: ^Self): IterNext(Next) {
self.n += 1
x := self.iter.inner.next()
return .(x.finished, .(self.n, x.val))
}
}
}
Take := fn($T: type): type {
$Next := @TypeOf(T.next(idk).val)
return struct {
iter: Iterator(T),
n: uint = 0,
end: uint,
next := fn(self: ^Self): IterNext(Next) {
self.n += 1
x := Type(IterNext(Next)).uninit()
if self.n > self.end return .(true, x.val)
return self.iter.inner.next()
}
}
}
Skip := fn($T: type): type {
$Next := @TypeOf(T.next(idk).val)
return struct {
iter: Iterator(T),
step: uint,
next := fn(self: ^Self): IterNext(Next) {
n := 0
loop {
x := self.iter.next()
if n == self.step | x.finished return x
n += 1
}
}
}
}
ChainState := enum {
Iter0,
Iter0Finished,
BothFinished,
}
Chain := fn($A: type, $B: type): type {
$Next := @TypeOf(A.next(idk).val)
$Next1 := @TypeOf(B.next(idk).val)
if Next1 != Next @error("Both iterators should return the same type")
return struct {
iter0: Iterator(A),
iter1: Iterator(B),
state: ChainState = .Iter0,
next := fn(self: ^Self): IterNext(Next) {
x := Type(IterNext(Next)).uninit()
match self.state {
.Iter0 => {
x = self.iter0.inner.next()
if x.finished {
self.state = .Iter0Finished
return self.next()
}
},
.Iter0Finished => {
x = self.iter1.inner.next()
if x.finished self.state = .BothFinished
},
_ => {
},
}
return .(self.state == .BothFinished, x.val)
}
}
}
IntersperseState := enum {
Iter0,
Iter1,
Iter0Finished,
Iter1Finished,
}
Intersperse := fn($A: type, $B: type): type {
$Next := @TypeOf(A.next(idk).val)
$Next1 := @TypeOf(B.next(idk).val)
if Next1 != Next @error("Both iterators should return the same type")
return struct {
iter0: Iterator(A),
iter1: Iterator(B),
state: IntersperseState = .Iter0,
next := fn(self: ^Self): IterNext(Next) {
x := Type(IterNext(Next)).uninit()
match self.state {
.Iter0 => {
x = self.iter0.inner.next()
if x.finished self.state = .Iter0Finished else self.state = .Iter1
},
.Iter1 => {
x = self.iter1.inner.next()
if x.finished {
self.state = .Iter1Finished
return self.next()
} else self.state = .Iter0
},
.Iter1Finished => {
x = self.iter0.inner.next()
if x.finished self.state = .Iter0Finished
},
_ => {
},
}
return .(self.state == .Iter0Finished, x.val)
}
}
}

99
src/lily/lib.hb Normal file
View file

@ -0,0 +1,99 @@
Version := struct {
major: uint,
minor: uint,
patch: uint,
}
$VERSION := Version(0, 0, 6)
Config := struct {
$DEBUG := true
$DEBUG_ASSERTIONS := false
$MIN_LOGLEVEL := log.LogLevel.Info
$debug := fn(): bool return Config.DEBUG
$debug_assertions := fn(): bool return Config.DEBUG | Config.DEBUG_ASSERTIONS
$min_loglevel := fn(): log.LogLevel {
if Config.debug() & Config.MIN_LOGLEVEL < .Debug return .Debug
return Config.MIN_LOGLEVEL
}
}
// ----------------------------------------------------
collections := @use("collections/lib.hb")
process := @use("process.hb")
result := @use("result.hb")
string := @use("string.hb")
alloc := @use("alloc/lib.hb")
hash := @use("hash/lib.hb")
rand := @use("rand/lib.hb")
math := @use("math.hb")
iter := @use("iter.hb")
log := @use("log.hb")
fmt := @use("fmt.hb");
.{Target} := @use("targets/lib.hb");
.{print, printf} := log;
.{Type, TypeOf} := @use("type.hb")
// ! (compiler) bug: inlining here crashes the parser. nice.
// ! (libc) (compiler) bug: NOT inlining here makes it sometimes not work
$panic := fn(message: ?[]u8): never {
if message != null log.error(message) else log.error("The program called panic.")
exit(1)
}
// ! exit, memcpy, memmove, and memset are all temporary wrapper functions
$exit := fn(code: int): never {
Target.exit(code)
die
}
$memcpy := fn(dest: @Any(), src: @Any(), size: uint): void {
if TypeOf(dest).kind() != .Pointer | TypeOf(src).kind() != .Pointer @error("memcpy requires a pointer")
Target.memcpy(@bitcast(dest), @bitcast(src), size)
}
$memmove := fn(dest: @Any(), src: @Any(), size: uint): void {
if TypeOf(dest).kind() != .Pointer | TypeOf(src).kind() != .Pointer @error("memmove requires a pointer")
Target.memmove(@bitcast(dest), @bitcast(src), size)
}
$memset := fn(dest: @Any(), src: u8, size: uint): void {
if TypeOf(dest).kind() != .Pointer @error("memset requires a pointer")
Target.memset(@bitcast(dest), src, size)
}
_qs_partition := fn($func: type, array: @Any(), start: uint, end: uint): uint {
pivot := array[end]
i := start
j := start
loop if j >= end break else {
defer j += 1
if func(array[j], pivot) {
temp := array[i]
array[i] = array[j]
array[j] = temp
i += 1
}
}
temp := array[i]
array[i] = array[end]
array[end] = temp
return i
}
/// Can sort in place if `&array` is passed rather than `array`
/// For sorting slices in place, do not pass `&slice`, pass `slice` instead.
quicksort := fn($func: type, array: @Any(), start: uint, end: uint): @TypeOf(array) {
if start >= end return array;
pivot_index := _qs_partition(func, array, start, end)
if pivot_index > 0 array = quicksort(func, array, start, pivot_index - 1)
array = quicksort(func, array, pivot_index + 1, end)
return array
}
$compare := fn(lhs: @Any(), rhs: @Any()): bool {
return lhs < rhs
}

53
src/lily/log.hb Normal file
View file

@ -0,0 +1,53 @@
.{Config, Target, fmt} := @use("lib.hb")
LogLevel := enum {
Error,
Warn,
Info,
Debug,
Trace,
}
$log := fn(level: LogLevel, str: []u8): void {
if level > Config.min_loglevel() return;
match Target.current() {
.LibC => match level {
.Error => Target.printf_str("\{1b}[31mERROR\{1b}[0m: %.*s\n\0".ptr, str.len, str.ptr),
.Warn => Target.printf_str("\{1b}[33mWARN\{1b}[0m: %.*s\n\0".ptr, str.len, str.ptr),
.Info => Target.printf_str("\{1b}[32mINFO\{1b}[0m: %.*s\n\0".ptr, str.len, str.ptr),
.Debug => Target.printf_str("\{1b}[34mDEBUG\{1b}[0m: %.*s\n\0".ptr, str.len, str.ptr),
.Trace => Target.printf_str("\{1b}[35mTRACE\{1b}[0m: %.*s\n\0".ptr, str.len, str.ptr),
},
.AbleOS => return @eca(3, 1, Target.LogMsg.(level, str.ptr, str.len), @sizeof(Target.LogMsg)),
}
}
// it's good enough i guess. dont write more than 4096 chars or you will explode.
print_buffer := @embed("assets/zeroed")
print := fn(any: @Any()): void {
if @TypeOf(any) == []u8 {
match Target.current() {
.LibC => Target.printf_str("%.*s\n\0".ptr, any.len, any.ptr),
.AbleOS => info(any),
}
} else {
// limits len to size of buffer - 1
len := fmt.format(print_buffer[0..@sizeof(@TypeOf(print_buffer)) - 1], any)
print_buffer[len] = 0
// ! (compiler) bug: not inlining here causes compiler panic
@inline(print, print_buffer[0..len])
}
}
printf := fn(str: []u8, any: @Any()): void {
len := fmt.format_with_str(str, print_buffer[0..@sizeof(@TypeOf(print_buffer)) - 1], any)
print_buffer[len] = 0
@inline(print, print_buffer[0..len])
}
$error := fn(message: []u8): void return log(.Error, message)
$warn := fn(message: []u8): void return log(.Warn, message)
$info := fn(message: []u8): void return log(.Info, message)
$debug := fn(message: []u8): void return log(.Debug, message)
$trace := fn(message: []u8): void return log(.Trace, message)

27
src/lily/math.hb Normal file
View file

@ -0,0 +1,27 @@
.{TypeOf} := @use("lib.hb")
// ! possibly little endian only.
// ! should be fixed if we rely on libc for math
// ! ableos is always little endian so no big deal.
$abs := fn(x: @Any()): @TypeOf(x) {
T := TypeOf(x)
if T.is_int() return (x ^ x >> @bitcast(T.bits()) - 1) - (x >> @bitcast(T.bits()) - 1)
if T.is_float() return @bitcast(@as(T.USize(), @bitcast(x)) & T.bitmask() >> 1)
@error("lily.math.abs only supports integers and floats.")
}
// todo: better float min, max
$min := fn(a: @Any(), b: @TypeOf(a)): @TypeOf(a) {
T := TypeOf(a)
if T.is_int() return b + (a - b & a - b >> @bitcast(T.bits()) - 1)
if T.is_float() return @itf(a > b) * b + @itf(a <= b) * a
}
$max := fn(a: @Any(), b: @TypeOf(a)): @TypeOf(a) {
T := TypeOf(a)
if T.is_int() return a - (a - b & a - b >> @bitcast(T.bits()) - 1)
if T.is_float() return @itf(a > b) * a + @itf(a <= b) * b
}
$clamp := fn(x: @Any(), minimum: @TypeOf(x), maximum: @TypeOf(x)): @TypeOf(x) {
return max(min(x, maximum), minimum)
}

3
src/lily/process.hb Normal file
View file

@ -0,0 +1,3 @@
.{Target} := @use("lib.hb")
$fork := fn(): uint return Target.fork()

1
src/lily/rand/lib.hb Normal file
View file

@ -0,0 +1 @@
.{SimpleRandom} := @use("simple.hb")

65
src/lily/rand/simple.hb Normal file
View file

@ -0,0 +1,65 @@
.{Target, TypeOf, Type} := @use("../lib.hb")
// ! NON CRYPTOGRAPHIC, TEMPORARY
SimpleRandom := struct {
seed: uint,
$new := fn(): Self return Self.(0)
$default := fn(): Self return Self.(0)
$deinit := fn(self: ^Self): void {
self.seed = 0
}
$any := fn(self: ^Self, $A: type): A {
T := Type(A)
match T.kind() {
.Slice => @error("Use SimpleRandom.fill or SimpleRandom.fill_values instead."),
.Array => @error("Use SimpleRandom.fill or SimpleRandom.fill_values instead."),
_ => {
},
}
if T.is_bool() {
a: A = idk
Target.getrandom(@bitcast(&a), 1)
return @bitcast(a & @as(u8, 1))
}
a: A = idk
Target.getrandom(@bitcast(&a), T.size())
return a
}
/// Accepts any type for min and max (as long as it is the same for both).
$range := fn(self: ^Self, min: @Any(), max: @TypeOf(min)): @TypeOf(min) {
return self.any(@TypeOf(min)) % (max - min) + min
}
/// Fills an array or slice with random bytes
$fill := fn(self: ^Self, buf: @Any()): void {
T := TypeOf(buf)
match T.kind() {
.Slice => Target.getrandom(@bitcast(buf.ptr), buf.len * T.Child().size()),
.Array => Target.getrandom(@bitcast(&buf), T.size()),
_ => @error("Can only fill bytes of Slice or Array."),
}
}
/// Fills an array or slice with random values
// ! (compiler) bug: `buf[i]` causing compiler panic here
fill_values := fn(self: ^Self, buf: @Any()): void {
T := TypeOf(buf)
match T.kind() {
.Slice => {
len := buf.len * T.Child().size()
i := 0
loop if i == len break else {
buf[i] = self.any(T.Child().This())
i += 1
}
},
.Array => {
len := T.size()
i := 0
$loop if i == len break else {
buf[i] = self.any(T.Child().This())
i += 1
}
},
_ => @error("Can only fill values of Slice or Array."),
}
}
}

16
src/lily/result.hb Normal file
View file

@ -0,0 +1,16 @@
.{panic} := @use("lib.hb")
ResultInner := fn($T: type, $E: type): type return union {ok: T, err: E}
Result := fn($T: type, $E: type): type return struct {
inner: ResultInner(T, E),
is_ok: bool,
$ok := fn(k: T): Self return .(.{ok: k}, true)
$err := fn(k: E): Self return .(.{err: k}, false)
$unwrap := fn(self: Self): T return self.expect("Unwrap on error variant.")
$unwrap_unchecked := fn(self: Self): T return self.inner.ok
unwrap_or := fn(self: Self, v: T): T if self.is_ok return self.inner.ok else return v
unwrap_or_else := fn(self: Self, $F: type): T if self.is_ok return self.inner.ok else return F(self.inner.err)
expect := fn(self: Self, msg: []u8): T if self.is_ok return self.inner.ok else panic(msg)
}

182
src/lily/string.hb Normal file
View file

@ -0,0 +1,182 @@
.{iter: .{Iterator, IterNext}, Type} := @use("lib.hb")
reverse := fn(str: []u8): void {
if str.len == 0 return;
j := str.len - 1
i := 0
temp := @as(u8, 0)
loop if i < j {
temp = str[i]
str[i] = str[j]
str[j] = temp
i += 1
j -= 1
} else return
}
equals := fn(lhs: []u8, rhs: []u8): bool {
if lhs.len != rhs.len return false
if lhs.ptr == rhs.ptr return true
i := 0
loop if i == lhs.len break else {
if lhs[i] != rhs[i] return false
i += 1
}
return true
}
clear := fn(str: []u8): void {
i := 0
loop if i == str.len break else {
str[i] = 0
i += 1
}
}
split_once := fn(haystack: []u8, needle: @Any()): ?struct {left: []u8, right: []u8} {
T := @TypeOf(needle)
i := 0
if T == []u8 {
if needle.len == 0 return null
loop {
if i + needle.len > haystack.len return null
if haystack[i] == needle[0] {
matches := true
n := 1
loop {
if n == needle.len break
if haystack[i + n] != needle[n] {
matches = false
break
}
n += 1
}
if matches return .(haystack[0..i], haystack[i + needle.len..])
}
i += 1
}
} else if T == u8 {
loop {
if haystack[i] == needle {
return .(haystack[0..i], haystack[i + 1..])
} else if i == haystack.len return null
i += 1
}
} else {
@error("Type of needle must be []u8 or u8.")
}
}
$split := fn(str: []u8, needle: @Any()): Iterator(struct {
str: []u8,
needle: @TypeOf(needle),
finished: bool = false,
next := fn(self: ^Self): IterNext([]u8) {
if self.finished return .(true, Type([]u8).uninit())
splits := split_once(self.str, self.needle)
if splits != null {
self.str = splits.right
return .(false, splits.left)
}
self.finished = true
return .(false, self.str)
}
}) {
T := @TypeOf(needle)
if T != []u8 & T != u8 {
@error("Type of needle must be []u8 or u8.")
}
return .(.{str, needle})
}
$chars := fn(iter: []u8): Iterator(struct {
str: []u8,
$next := fn(self: ^Self): IterNext(u8) {
tmp := IterNext(u8).(self.str.len == 0, *self.str.ptr)
self.str = self.str[1..]
return tmp
}
}) {
return .(.(iter))
}
$chars_ref := fn(iter: []u8): Iterator(struct {
str: []u8,
$next := fn(self: ^Self): IterNext(^u8) {
tmp := IterNext(^u8).(self.str.len == 0, self.str.ptr)
self.str = self.str[1..]
return tmp
}
}) {
return .(.(iter))
}
count := fn(haystack: []u8, needle: @Any()): uint {
T := @TypeOf(needle)
i := 0
c := 0
if T == []u8 {
if needle.len == 0 return null
loop {
if i + needle.len > haystack.len return c
if haystack[i] == needle[0] {
matches := true
n := 1
loop {
if n == needle.len break
if haystack[i + n] != needle[n] {
matches = false
break
}
n += 1
}
if matches c += 1
}
i += 1
}
} else if T == u8 {
loop {
if haystack[i] == needle c += 1 else if i == haystack.len return c
i += 1
}
} else {
@error("Type of needle must be []u8 or u8.")
}
}
left_trim := fn(str: []u8, sub: []u8): []u8 {
i := 0
if str[0] == sub[0] {
loop if i == sub.len {
return str[i..str.len]
} else if str[i] != sub[i] | i == str.len {
break
} else {
i += 1
}
}
return str
}
right_trim := fn(str: []u8, sub: []u8): []u8 {
i := 0
if str[str.len - 1] == sub[sub.len - 1] {
loop if i == sub.len {
return str[0..str.len - i]
} else if str[str.len - i - 1] != sub[sub.len - i - 1] | i == str.len {
break
} else {
i += 1
}
}
return str
}
trim := fn(str: []u8, sub: []u8): []u8 {
return right_trim(left_trim(str, sub), sub)
}

View file

@ -0,0 +1,51 @@
# documenting the used features of the AbleOS spec
> [!Important]
> this does not apply to all hbvm targets. it applies to ableos specifically. other hbvm targets will have different ecalls, or even no ecalls. for an example of another project using hbvm, see [depell](https://depell.mlokis.tech/) (dependency hell), a website created by mlokis, the main programmer of hblang, that runs hblang programs, in hbvm, in wasm.
## how do ecalls work?
ecalls are comprised of a series of values, with each consecutive one representing the value of a vm register from 1-255 (the 0 register is reserved). `ecall a b c` fills the first three registers with the values `a`, `b`, and `c` respectively. the ecall handler reads these values and performs a kernel operation based on them.
## how is this formatted?
all registers are followed by parethesis with their purpose. the `message` section is actually a pointer to a single location in memory, taking a single register. the following register is always the size of this message. this is omitted for brevity.<br>
`ecall register(purpose), ..., register(purpose), message_bytes:type, ..., message_bytes:type`
## more info?
read [here](https://git.ablecorp.us/AbleOS/ableos/src/branch/master/kernel/src/holeybytes/ecah.rs) for the full set of ecalls and buffer ids.
### `lily.log`:
log: `ecall 3(buf), 1(log), loglevel:u8, string:*const u8, strlen:u64`<br>
> formats and then copies `strlen` of `string` into the serial output
### `lily.Target.AbleOS`:
alloc: `ecall 3(buf), 2(mem), 0(alloc), page_count:u64, zeroed:bool=false`
> returns `Option<*mut u8>` to an available contiguous chunk of memory, sized in `4096` byte (align `8`) pages. it is undefined behaviour to use size zero.
alloc_zeroed: `ecall 3(buf), 2(mem), 0(alloc), page_count:u64, zeroed:bool=true`<br>
> same as alloc, except filled with zeroes.
realloc: `ecall 3(buf), 2(mem), 7(realloc), page_count:u64, page_count_new:u64, ptr:*const u8, ptr_new:*const u8`<br>
> resizes an existing contiguous chunk of memory allocated via `alloc`, `alloc_zeroed`, or `realloc`. contents remain the same. it is undefined behaviour to use size zero or a `null` pointer. returns a new `Option<*mut u8>` after resizing.
dealloc: `ecall 3(buf), 2(mem), 1(dealloc), page_count:u64, ptr:*const u8`<br>
> releases an existing contiguous chunk of memory allocated via `alloc`, `alloc_zeroed`, or `realloc`. it is undefined behaviour to use size zero or a `null` pointer.
memcpy: `ecall 3(buf), 2(mem), 4(memcopy), size:u64, src:*const u8, dest:*const u8`<br>
> copies `size` of `src` into `dest`. `src` and `dest` must not be overlapping. it is undefined behaviour to use size zero or a `null` pointer.
memset: `ecall 3(buf), 2(mem), 5(memset), count:u64, size:u64, src:*const u8, dest:*mut u8`<br>
> consecutively copies `size` of `src` into `dest` a total of `count` times. `src` and `dest` must not be overlapping. it is undefined behaviour to use size zero or a `null` pointer.
memmove: `ecall 3(buf), 2(mem), 6(memmove), size:u64, src:*const u8, dest:*mut u8`<br>
> copies `size` of `src` into a buffer, then from the buffer into `dest`. `src` and `dest` can be overlapping. it is undefined behaviour to use size zero or a `null` pointer.
getrandom: `ecall 3(buf) 4(rand) dest:*mut u8, size:u64`
> fills `dest` with `size` bytes of random cpu entropy.
## ecall numbers (u8)
3. send a message to a buffer
## buffer ids (uint)
1. logging service
2. memory service
4. random service

View file

@ -0,0 +1,51 @@
.{LogLevel} := @use("../lib.hb").log
$page_size := fn(): uint {
return 4096
}
LogMsg := packed struct {level: LogLevel, string: ^u8, strlen: uint}
$calculate_pages := fn(size: uint): uint {
return (size + page_size() - 1) / page_size()
}
AllocMsg := packed struct {a: u8, count: uint, zeroed: bool}
$alloc := fn(size: uint): ?^u8 {
return @eca(3, 2, &AllocMsg.(0, calculate_pages(size), false), @sizeof(AllocMsg))
}
$alloc_zeroed := fn(size: uint): ?^u8 {
return @eca(3, 2, &AllocMsg.(0, calculate_pages(size), true), @sizeof(AllocMsg))
}
ReallocMsg := packed struct {a: u8, count: uint, count_new: uint, ptr: ^u8}
$realloc := fn(ptr: ^u8, size: uint, size_new: uint): ?^u8 {
return @eca(3, 2, &ReallocMsg.(7, calculate_pages(size), calculate_pages(size_new), ptr), @sizeof(ReallocMsg))
}
FreeMsg := packed struct {a: u8, count: uint, ptr: ^u8}
$dealloc := fn(ptr: ^u8, size: uint): void {
return @eca(3, 2, &FreeMsg.(1, calculate_pages(size), ptr), @sizeof(FreeMsg))
}
CopyMsg := packed struct {a: u8, count: uint, src: ^u8, dest: ^u8}
$memcpy := fn(dest: ^u8, src: ^u8, size: uint): void {
return @eca(3, 2, &CopyMsg.(4, size, src, dest), @sizeof(CopyMsg))
}
SetMsg := packed struct {a: u8, count: uint, size: uint, src: ^u8, dest: ^u8}
$memset := fn(dest: ^u8, src: u8, size: uint): void {
return @eca(3, 2, &SetMsg.(5, size, 1, @bitcast(&src), dest), @sizeof(SetMsg))
}
$memmove := fn(dest: ^u8, src: ^u8, size: uint): void {
return @eca(3, 2, &CopyMsg.(6, size, src, dest), @sizeof(CopyMsg))
}
$getrandom := fn(dest: ^u8, size: uint): void return @eca(3, 4, dest, size)
$exit := fn(code: int): void {
}
$fork := fn(): uint return @eca(3, 7)

26
src/lily/targets/lib.hb Normal file
View file

@ -0,0 +1,26 @@
Target := enum {
LibC,
AbleOS,
ableos := @use("ableos.hb")
libc := @use("libc.hb")
$current := fn(): Self {
// This captures all HBVM targets, but for now only AbleOS is supported
if @target("*-virt-unknown") {
return .AbleOS
}
// Assume that unknown targets have libc
return .LibC
}
$Lib := fn(self: Self): type {
match self {
.AbleOS => return Self.ableos,
.LibC => return Self.libc,
}
}
/* todo: reorganise these */;
.{alloc, alloc_zeroed, realloc, dealloc, memmove, memcpy, memset, exit, getrandom, page_size, calculate_pages, fork} := Self.Lib(Self.current());
.{printf_str} := Self.Lib(.LibC);
.{LogMsg} := Self.Lib(.AbleOS)
}

27
src/lily/targets/libc.hb Normal file
View file

@ -0,0 +1,27 @@
alloc := fn(size: uint): ?^u8 @import("malloc")
alloc_zeroed := fn(size: uint): ?^u8 @import("calloc")
realloc_c := fn(ptr: ^u8, size: uint): ?^u8 @import("realloc")
dealloc_c := fn(ptr: ^u8): void @import("free")
memmove := fn(dest: ^u8, src: ^u8, size: uint): void @import()
memcpy := fn(dest: ^u8, src: ^u8, size: uint): void @import()
memset := fn(dest: ^u8, src: u8, size: uint): void @import()
exit := fn(code: int): void @import()
printf_str := fn(str0: ^u8, strlen: uint, str1: ^u8): void @import("printf")
getrandom := fn(dest: ^u8, size: uint): void @import()
fork := fn(): uint @import()
$realloc := fn(ptr: ^u8, size: uint, size_new: uint): ?^u8 {
return realloc_c(ptr, size)
}
$dealloc := fn(ptr: ^u8, size: uint): void {
return dealloc_c(ptr)
}
// temp
$page_size := fn(): uint {
return 4096
}
// also temp
$calculate_pages := fn(size: uint): uint {
return (size + page_size() - 1) / page_size()
}

105
src/lily/type.hb Normal file
View file

@ -0,0 +1,105 @@
RawKind := enum {
Builtin,
Struct,
Tuple,
Enum,
Union,
Pointer,
Slice,
Optional,
Function,
Template,
Global,
Constant,
Module,
}
Kind := enum {
Builtin,
Struct,
Tuple,
Enum,
Union,
Pointer,
Slice,
Array,
Optional,
Function,
Template,
Global,
Constant,
Module,
}
TypeOf := fn(T: @Any()): type return Type(@TypeOf(T))
Type := fn($T: type): type return struct {
// ! no way of representing arbitrary size integers yet
USize := fn(): type {
if @sizeof(T) == 1 return u8 else if @sizeof(T) == 2 return u16 else if @sizeof(T) == 4 return u32 else return uint
}
Child := fn(): type {
return Type(@ChildOf(T))
}
This := fn(): type {
return T
}
$name := fn(): []u8 {
return @nameof(T)
}
$is_bool := fn(): bool {
return T == bool
}
$is_unsigned_int := fn(): bool {
return T == uint | T == u8 | T == u16 | T == u32
}
$is_signed_int := fn(): bool {
return T == int | T == i8 | T == i16 | T == i32
}
$is_int := fn(): bool {
return Self.is_unsigned_int() | Self.is_signed_int()
}
$is_float := fn(): bool {
return T == f32 | T == f64
}
$len := fn(): uint {
return @lenof(T)
}
$align := fn(): uint {
return @alignof(T)
}
$size := fn(): uint {
return @sizeof(T)
}
$bits := fn(): Self.USize() {
return @sizeof(T) << 3
}
$bitmask := fn(): Self.USize() {
return -1
}
$raw_kind := fn(): RawKind {
return @bitcast(@kindof(T))
}
$kind := fn(): Kind {
if Self.raw_kind() == .Slice {
if []@ChildOf(T) == T return .Slice else return .Array
} else if @kindof(T) > @bitcast(Kind.Slice) {
return @bitcast(@kindof(T) + 1)
} else return @bitcast(Self.raw_kind())
}
/// ! There are no guarantees that this value is zeroed for builtins, enums, unions, structs, arrays, or tuples.
$uninit := fn(): T {
match Self.kind() {
.Pointer => return @bitcast(0),
.Slice => return Type(^Self.Child().This()).uninit()[0..0],
.Array => return idk,
.Builtin => return idk,
.Struct => return idk,
.Tuple => return idk,
.Union => return idk,
.Enum => return idk,
.Optional => return null,
_ => @error("Type(", T, ").uninit() does not make sense."),
}
}
}

View file

@ -1,37 +0,0 @@
.{target, config, fmt} := @use("lib.hb")
LogLevel := enum {
.Error;
.Warn;
.Info;
.Debug;
.Trace;
}
$log := fn(level: LogLevel, str: []u8): void {
if level > config.min_loglevel() {
return
}
$match target.current() {
.AbleOS => return @ecall(3, 1, target.LogEcall.(level, str.ptr, str.len), @size_of(target.LogEcall)),
}
}
$error := fn(message: []u8): void return log(.Error, message)
$warn := fn(message: []u8): void return log(.Warn, message)
$info := fn(message: []u8): void return log(.Info, message)
$debug := fn(message: []u8): void return log(.Debug, message)
$trace := fn(message: []u8): void return log(.Trace, message)
fmt_buffer: [config.FMT_BUFFER_SIZE]u8 = idk
print := fn(any: @Any()): void {
$if @TypeOf(any) == []u8 {
$match target.current() {
.AbleOS => info(any),
}
} else {
len := fmt.format(fmt_buffer[..], any)
info(fmt_buffer[..len])
}
}

83
src/main.hb Normal file
View file

@ -0,0 +1,83 @@
lily := @use("lily/lib.hb");
Generator := struct {
n: uint = 0,
$next := fn(self: ^Self): lily.iter.IterNext(uint) {
self.n += 1
return .(false, self.n)
}
$into_iter := fn(self: Self): lily.iter.Iterator(Self) {
return .(self)
}
}
$add := fn(sum: uint, x: uint): uint {
return sum + x
}
main := fn(argc: uint, argv: []^void): uint {
// sum := Generator.{}.into_iter().take(50).fold(add, 0)
// lily.print(sum)
// // ! (libc) (compiler) bug: .collect(T) does not work.
// if lily.Target.current() != .LibC {
// str := lily.string.chars("Hello, ").intersperse(
// lily.string.chars("World!"),
// ).collect([13]u8)
// if str != null {
// lily.log.info(@as([13]u8, str)[..])
// } else {
// lily.panic("could not collect (array wrong size)")
// }
// } else {
// // yes, im cheating if you are on libc.
// // it's not my fault, blame compiler bugs. T^T
// lily.log.info("HWeolrllod,! ")
// }
// return 0
/* ! the following will ONLY work on ableos
* due to fun compiler bugs
*/
// allocator := lily.alloc.SimpleAllocator.new()
// defer allocator.deinit()
// map := lily.collections.HashMap(
// uint,
// uint,
// lily.hash.FoldHasher,
// lily.alloc.SimpleAllocator,
// ).new(&allocator)
// defer map.deinit()
// i := 0
// $loop if i == 99 * 2 break else {
// _ = map.insert(i, 0)
// _ = map.insert(i + 1, 0)
// i += 2
// }
// map.keys().for_each(lily.print)
// fun thing
// _ = map.insert("Farewell, World!", "beep boop")
// _ = map.insert("Hello, World!", "Hello!")
// _ = map.insert("Goodbye, World!", "Goodbye!")
// _ = map.insert("How do you do, World?", "Great!")
// _ = map.insert("Until next time, World!", "See you!")
// _ = map.insert("Greetings, World!", "Hi there!")
// lily.print(map.get("asdfasdf!"))
// lily.print(map.get("Hello, World!"))
id := lily.process.fork()
lily.print("hello, world")
if id == 0 {
lily.print("child")
} else {
lily.print("parent")
}
return 0
}

View file

@ -1,117 +0,0 @@
.{target, iter: .{Iterator, Next}, config, log, Type} := @use("lib.hb")
$size := fn($T: type, count: uint): uint {
return @size_of(T) * count
}
/// safety: assumes align != 0
$forward_align := fn(ptr: ^u8, _align: uint): ^u8 {
return @bit_cast((@bit_cast(ptr) + _align - 1) / _align * _align)
}
/// safety: assumes align != 0
$backward_align := fn(ptr: ^u8, _align: uint): ^u8 {
return @bit_cast(@bit_cast(ptr) / _align * _align)
}
$is_aligned := fn(ptr: ^u8, _align: uint): bool {
return @bit_cast(ptr) % _align == 0
}
$dangling := fn($T: type): ^T {
$if Type(T).kind() == .Optional @error(T, " is an optional pointer. use `null` instead.")
return @bit_cast(@align_of(T))
}
$as_bytes := fn(v: @Any()): []u8 {
$T := @TypeOf(v)
$match Type(T).kind() {
.Pointer => return @as(^u8, @bit_cast(v))[..@size_of(@ChildOf(T))],
.Slice => return @as(^u8, @bit_cast(v.ptr))[..@size_of(@ChildOf(T)) * v.len],
_ => @error(@TypeOf(v), " is not a pointer or a slice."),
}
}
$to_owned := fn($T: type, slice: []u8): T {
$match Type(T).kind() {
.Array => {
ret: T = idk
copy(ret[..], slice)
return ret
},
_ => @error("todo: write this error"),
}
}
$overlaps := fn(lhs: []u8, rhs: []u8): bool {
return lhs.ptr < rhs.ptr + rhs.len & rhs.ptr < lhs.ptr + lhs.len
}
$copy := fn(dest: []u8, src: []u8): void {
$if config.DEBUG {
if src.len > dest.len | overlaps(dest, src) {
log.error("mem.copy: regions overlap or src bigger than dest")
die
}
}
target.memcopy(dest.ptr, src.ptr, src.len)
}
$move := fn(dest: []u8, src: []u8): void {
$if config.DEBUG {
log.error("mem.move: src bigger than dest")
if src.len > dest.len die
}
target.memmove(dest.ptr, src.ptr, src.len)
}
$set := fn(dest: []u8, src: u8): void {
target.memset(dest.ptr, src, dest.len)
}
$fill := fn(dest: []u8, src: []u8): void {
$if config.DEBUG {
if src.len > dest.len | overlaps(dest, src) {
log.error("mem.copy: regions overlap or src bigger than dest or align bad")
die
}
}
target.memfill(dest.ptr, src.ptr, dest.len / src.len, src.len)
}
equals := fn(lhs: []u8, rhs: []u8): bool {
if lhs.len != rhs.len return false
if lhs.ptr == rhs.ptr return true
i := 0
loop if i == lhs.len break else {
if lhs[i] != rhs[i] return false
i += 1
}
return true
}
reverse := fn(slice: []u8): []u8 {
if slice.len == 0 return slice
j := slice.len - 1
i := 0
temp: u8 = 0
loop if i < j {
temp = slice[i]
slice[i] = slice[j]
slice[j] = temp
i += 1
j -= 1
} else return slice
}
$iter := fn(slice: []u8): Iterator(struct {
.slice: []u8
$next := fn(self: ^@CurrentScope()): Next(u8) {
tmp := Next(u8).(self.slice.len == 0, self.slice.ptr.*)
self.slice = self.slice[1..]
return tmp
}
}) {
return .(.(slice))
}

View file

@ -1,24 +0,0 @@
lily.{target} := @use("lib.hb")
ProcessID := fn(): type {
$match target.current() {
.AbleOS => return struct {
.host_id: uint;
.id: uint;
},
}
}
$HOST_ID_PLACEHOLDER := 0
$spawn := fn(executable: []u8): ProcessID() {
raw := target.proc_spawn(executable)
// todo: this
return .(HOST_ID_PLACEHOLDER, raw)
}
$fork := fn(): ProcessID() {
raw := target.proc_fork()
// todo: this
return .(HOST_ID_PLACEHOLDER, raw)
}

View file

@ -1,81 +0,0 @@
.{LogLevel} := @use("../lib.hb").log
LogEcall := struct align(1){.level: LogLevel; .str_ptr: ^u8; .str_len: uint}
$page_len := fn(): uint {
return 4096
}
$pages := fn(len: uint): uint {
return (len + page_len() - 1) / page_len()
}
AllocEcall := struct align(1){.pad: u8; .pages_new: uint; .zeroed: bool}
$alloc := fn(len: uint): ?^u8 {
return @ecall(3, 2, AllocEcall.(0, pages(len), false), @size_of(AllocEcall))
}
$alloc_zeroed := fn(len: uint): ?^u8 {
return @ecall(3, 2, AllocEcall.(0, pages(len), true), @size_of(AllocEcall))
}
ReallocEcall := struct align(1){.pad: u8; .pages_old: uint; .pages_new: uint; .ptr_old: ^u8}
$realloc := fn(ptr_old: ^u8, len_old: uint, len_new: uint): ?^u8 {
return @ecall(3, 2, ReallocEcall.(7, pages(len_old), pages(len_new), ptr_old), @size_of(ReallocEcall))
}
DeallocEcall := struct align(1){.pad: u8; .pages: uint; .ptr: ^u8}
$dealloc := fn(ptr: ^u8, len: uint): void {
@ecall(3, 2, DeallocEcall.(1, pages(len), ptr), @size_of(DeallocEcall))
}
CopyEcall := struct align(1){.pad: u8; .len: uint; .src: ^u8; .dest: ^u8}
$memcopy := fn(dest: ^u8, src: ^u8, len: uint): void {
@ecall(3, 2, CopyEcall.(4, len, src, dest), @size_of(CopyEcall))
}
$memmove := fn(dest: ^u8, src: ^u8, len: uint): void {
@ecall(3, 2, CopyEcall.(6, len, src, dest), @size_of(CopyEcall))
}
FillEcall := struct align(1){.pad: u8; .count: uint; .len: uint; .src: ^u8; .dest: ^u8}
$memset := fn(dest: ^u8, src: u8, len: uint): void {
@ecall(3, 2, FillEcall.(5, len, 1, &src, dest), @size_of(FillEcall))
}
$memfill := fn(dest: ^u8, src: ^u8, count: uint, len: uint): void {
@ecall(3, 2, FillEcall.(5, count, len, src, dest), @size_of(FillEcall))
}
$exit := fn(code: u8): void {
}
$rand_fill := fn(dest: ^u8, len: uint): void return @ecall(3, 4, dest, len)
$proc_fork := fn(): uint return @ecall(3, 7)
$proc_spawn := fn(executable: []u8): uint {
return @ecall(3, 6, executable.ptr, executable.len)
}
$dt_get := fn($T: type, query: []u8): T {
return @ecall(3, 5, query.ptr, query.len)
}
BufferEcall := struct align(1){.operation: u8; .str_ptr: ^u8; .str_len: uint}
$buf_create_named := fn(name: []u8): uint {
return @ecall(3, 0, BufferEcall.(0, name.ptr, name.len), @size_of(BufferEcall))
}
$buf_create := fn(): uint {
return @ecall(1, 0)
}
$buf_destroy := fn(id: uint): void {
return @ecall(2, id)
}
$buf_search := fn(name: []u8): uint {
return @ecall(3, 0, BufferEcall.(3, name.ptr, name.len), @size_of(BufferEcall))
}
$buf_await := fn(id: uint): void {
return @ecall(7, id)
}
$buf_read := fn(id: uint, mmap: []u8): void {
return @ecall(4, id, mmap.ptr, mmap.len)
}
$buf_write := fn(id: uint, mmap: []u8): void {
return @ecall(3, id, mmap.ptr, mmap.len)
}

View file

@ -1,42 +0,0 @@
lib.{
LogEcall,
pages,
page_len,
alloc,
alloc_zeroed,
realloc,
dealloc,
memcopy,
memmove,
memset,
memfill,
exit,
rand_fill,
proc_fork,
proc_spawn,
buf_create_named,
buf_create,
buf_destroy,
buf_search,
buf_await,
buf_read,
buf_write,
} := Lib(current())
Target := enum {
.AbleOS;
}
current := fn(): Target {
$if @target("ableos") {
return .AbleOS
} else {
@error("Unknown Target")
}
}
Lib := fn(target: Target): type {
$match target {
.AbleOS => return @use("ableos.hb"),
}
}

73
src/test/lang/segfault.hb Normal file
View file

@ -0,0 +1,73 @@
/*
* exit: 0
*/
RawKind := enum {
Builtin,
Struct,
Tuple,
Enum,
Union,
Pointer,
Slice,
Optional,
Function,
Template,
Global,
Constant,
Module,
}
Kind := enum {
Builtin,
Struct,
Tuple,
Enum,
Union,
Pointer,
Slice,
Array,
Optional,
Function,
Template,
Global,
Constant,
Module,
}
Type := fn($T: type): type return struct {
Child := fn(): type {
return Type(@ChildOf(T))
}
This := fn(): type {
return T
}
$raw_kind := fn(): RawKind {
return @bitcast(@kindof(T))
}
$kind := fn(): Kind {
if Self.raw_kind() == .Slice {
if []@ChildOf(T) == T return .Slice else return .Array
} else if @kindof(T) > @bitcast(Kind.Slice) {
return @bitcast(@kindof(T) + 1)
} else return @bitcast(Self.raw_kind())
}
/// ! There are no guarantees that this value is zeroed for builtins, enums, unions, structs, arrays, or tuples.
$uninit := fn(): T {
match Self.kind() {
.Pointer => return @bitcast(0),
.Slice => return Type(^Self.Child().This()).uninit()[0..0],
_ => @error("Type(", T, ").uninit() does not make sense."),
}
}
}
Vec := struct {
slice: []u8,
$new := fn(): Self return .{slice: Type([]u8).uninit()}
push := fn(self: ^Self, value: u8): void {
self.slice[self.slice.len] = value
}
}
main := fn(): u8 {
foo := Vec.new()
foo.push(6)
return 0
}

7
src/test/lang/trivial.hb Normal file
View file

@ -0,0 +1,7 @@
/*
* exit: 0
*/
main := fn(): u8 {
return 0
}

View file

@ -1,13 +1,13 @@
expectations := .{
return_value: 0,
}
/*
* exit: 0
*/
sum_days := uint.[0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365]
sum_days := .[0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365]
unix_timestamp_secs_lookup_table := fn(year: uint, month: uint, day: uint, hour: uint, minute: uint, second: uint): uint {
is_leap := year % 4 == 0 & (year % 100 != 0 | year % 400 == 0)
days_since_epoch := year * 365 + (year - 1) / 4 - (year - 1) / 100 + (year - 1) / 400 - 719527
total_days := days_since_epoch + day + sum_days[month - 1] + is_leap * (month > 2) - 1
total_days := days_since_epoch + day + sum_days[month - 1] + is_leap * (month > 2) - 1;
return total_days * 86400 + hour * 3600 + minute * 60 + second
}
@ -34,4 +34,4 @@ main := fn(): bool {
r |= unix_timestamp_secs_lookup_table(2025, 1, 31, 21, 53, 26) != unix_timestamp_secs(2025, 1, 31, 21, 53, 26)
r |= @inline(unix_timestamp_secs_lookup_table, 2025, 1, 31, 21, 53, 26) != unix_timestamp_secs(2025, 1, 31, 21, 53, 26)
return r
}
}

View file

@ -0,0 +1,17 @@
/*
* exit: 0
*/
lily := @use("../../lily/lib.hb")
main := fn(): u8 {
allocator := lily.alloc.ArenaAllocator.new()
defer allocator.deinit()
b := allocator.alloc(u8, 100)
if b == null return 1
c := allocator.alloc(u8, 100)
if c == null return 1
d := allocator.realloc(u8, c, 100)
if d == null return 1
if d != c return 1
return 0
}

View file

@ -0,0 +1,18 @@
/*
* exit: 0
*/
lily := @use("../../lily/lib.hb")
main := fn(): u8 {
allocator := lily.alloc.RawAllocator.new()
defer allocator.deinit()
b := allocator.alloc(u8, 100)
if b == null return 1
c := allocator.alloc(u8, 100)
if c == null return 1
d := allocator.realloc(u8, c, 100)
if d == null return 1
if d != c return 1
return 0
}

View file

@ -0,0 +1,11 @@
/*
* stdout: Hello, World!
* exit: 0
*/
lily := @use("../../lily/lib.hb")
main := fn(): u8 {
lily.log.print("Hello, World!")
return 0
}

View file

@ -1,91 +0,0 @@
RawKind := enum {
.Builtin;
.Pointer;
.SliceOrArray;
.Optional;
.Tuple;
.Enum;
.Union;
.Struct;
.Template;
.Function;
.Global;
}
Kind := enum {
.Builtin;
.Pointer;
.Slice;
.Array;
.Optional;
.Tuple;
.Enum;
.Union;
.Struct;
.Template;
.Function;
.Global;
}
TypeOf := fn(v: @Any()): type return Type(@TypeOf(v))
Type := fn($T: type): type return struct {
Self := @CurrentScope()
USize := fn(): type {
$if @size_of(T) == 0 @error(T, "(size=", @size_of(T), ")", "is too small to fit into an integer.")
$if @size_of(T) == 1 return u8 else $if @size_of(T) == 2 return u16 else $if @size_of(T) <= 4 return u32 else $if @size_of(T) <= 8 return uint else @error(T, "(size=", @size_of(T), ")", "is too big to fit into an integer.")
}
Child := fn(): type {
return Type(@ChildOf(T))
}
This := fn(): type {
return T
}
$name := fn(): []u8 {
return @name_of(T)
}
$is_bool := fn(): bool {
return T == bool
}
$is_unsigned_int := fn(): bool {
return T == uint | T == u8 | T == u16 | T == u32
}
$is_signed_int := fn(): bool {
return T == int | T == i8 | T == i16 | T == i32
}
$is_int := fn(): bool {
return is_unsigned_int() | is_signed_int()
}
$is_float := fn(): bool {
return T == f32 | T == f64
}
$len := fn(): uint {
return @len_of(T)
}
$align := fn(): uint {
return @align_of(T)
}
$size := fn(): uint {
return @size_of(T)
}
$bits := fn(): USize() {
return @size_of(T) << 3
}
$bitmask := fn(): USize() {
return ~0
}
/// `RawKind` does not disambiguate `.SliceOrArray`
$raw_kind := fn(): RawKind {
return @bit_cast(@kind_of(T))
}
/// `Kind` disambiguates `.Slice` and `.Array`
$kind := fn(): Kind {
$match raw_kind() {
.SliceOrArray => $if []@ChildOf(T) == T return .Slice else return .Array,
_ => $if @kind_of(T) > @bit_cast(RawKind.SliceOrArray) {
return @bit_cast(@kind_of(T) + 1)
} else return @bit_cast(@kind_of(T)),
}
}
}