iterator, formatting, memory, & testing stuff

This commit is contained in:
koniifer 2025-03-15 10:09:29 +00:00
parent 93cefa516f
commit cbc28723a2
22 changed files with 283 additions and 84 deletions

63
build
View file

@ -4,18 +4,19 @@ 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/test}"
readonly LILY_TEST_DIR="${LILY_TEST_DIR:-$LILY_SCRIPT_DIR/hbc-tests}"
readonly LILY_BUILD_DIR="${LILY_BUILD_DIR:-$LILY_SCRIPT_DIR/out}"
readonly HBLANG_GIT="https://git.ablecorp.us/mlokis/hblang"
readonly HBLANG_COMMIT="78bb8ef107d6fa9944632eee98863b4d10023396"
readonly HBLANG_COMMIT="ee74cf6b3a7bf5b0c149147c737ab685d4dc215e"
readonly HBC_FLAGS="--path-projection lily $LILY_SRC_DIR/lib.hb ${HBC_FLAGS:-}"
readonly HBC_BINARY="hbc"
readonly HBC_TESTS="${HBC_TESTS:-0}"
die() { error "$2" && exit "$1"; }
error() { printf "\033[31mERROR\033[0m: %s\n" "$1" 1>&2; }
log() { printf "\033[32mINFO\033[0m: %s\n" "$1" 1>&2; }
warn() { printf "\033[33mWARN\033[0m: %s\n" "$1" 1>&2; }
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"; }
fetch_step() {
command -v zig >/dev/null 2>&1 || die 101 "'zig' binary not found in PATH"
@ -46,7 +47,8 @@ fetch_step() {
git checkout -f "$HBLANG_COMMIT" >/dev/null 2>&1 || die 1 "failed to checkout commit"
fi
zig build install >/dev/null 2>&1 || die 1 "failed to build hblang"
# do not build with optimisations if you want to report bugs.
zig build install >/dev/null || die $? "failed to build hblang. exit code=$?"
PATH="$hblang_dir/zig-out/bin:$PATH"
cd "$LILY_SCRIPT_DIR" || die 1 "failed to return to script directory"
@ -56,11 +58,56 @@ main() {
mkdir -p "$LILY_BUILD_DIR" || die 1 "failed to create build directory"
fetch_step
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=$(basename "$file" .hb)
test_name=$(echo "$(basename -- "$(dirname -- "$file")")/$(basename -- "$file" .hb)" | tr '/' '.')
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"; then
if $HBC_BINARY $HBC_FLAGS "$inp" >"$LILY_BUILD_DIR/out.axe"; then
$HBC_BINARY $HBC_FLAGS "$inp" --fmt >/dev/null 2>&1
else
exit $?

View file

@ -1,15 +1,15 @@
expectations := .{
return_value: 0,
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,5 +1,5 @@
expectations := .{
return_value: 0,
return_value: 0,
}
generic := fn(v: @Any()): uint {

View file

@ -1,5 +1,5 @@
expectations := .{
return_value: 0,
return_value: 0,
}
index := fn(buf: @Any()): u8 {

View file

@ -1,5 +1,5 @@
expectations := .{
return_value: 0,
return_value: 0,
}
opaque := fn(v: @Any()): bool {

View file

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

View file

@ -1,5 +1,5 @@
expectations := .{
return_value: 0,
return_value: 0,
}
Vec := struct {

View file

@ -12,8 +12,6 @@ Struct := struct {
main := fn(): uint {
a := Struct.(null)
// ! (compiler) bug: adding type here makes
// ! self get passed by value and not by ref
a.modify(void)
if a.inner == null return 1

View file

@ -1,5 +1,5 @@
expectations := .{
return_value: 0,
return_value: 0,
}
opaque := fn(): ?^u8 {
@ -20,4 +20,4 @@ main := fn(): u8 {
return 1
}
return 0
}
}

View file

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

View file

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

View file

@ -1,5 +1,5 @@
expectations := .{
return_value: 0,
return_value: 0,
}
sum_days := uint.[0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365]
@ -7,7 +7,7 @@ sum_days := uint.[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

@ -1,11 +1,11 @@
expectations := .{
return_value: 5,
ecalls: .(
.(3, 2): 1,
),
ecalls: .(
.(3, 2): 1,
),
}
lily.{fmt, log, mem, alloc, target} := @use("../../src/lib.hb")
lily.{mem, alloc} := @use("../../src/lib.hb")
main := fn(): uint {
arena := alloc.Arena.new()
@ -13,7 +13,7 @@ main := fn(): uint {
_ = arena.alloc(u8, 1).?
iter := mem.bytes(mem.reverse("Hello, World!")[1..]).take(5)
iter := mem.iter(mem.reverse("Hello, World!")[1..]).take(5)
str := iter.collect_vec(&arena)
return str.len()
}

18
hbc-tests/lily/fmt.hb Normal file
View file

@ -0,0 +1,18 @@
expectations := .{
return_value: 0,
}
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

@ -0,0 +1,14 @@
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,14 +1,10 @@
lily.{fmt, log, mem, alloc, target} := @use("lily")
b: []u8 = &.[]
main := fn(): void {
arena := alloc.Arena.new()
defer arena.deinit()
b = arena.alloc(u8, 1).?
iter := mem.bytes(mem.reverse("Hello, World!")[1..]).take(5)
iter := mem.iter(mem.reverse("Hello, World!")[1..]).take(5)
str := iter.collect_vec(&arena)
log.info(str.slice)
}

View file

@ -42,7 +42,6 @@ Arena := struct {
}
loop {
// lily.log.debug("arena.hb:45: if i dont print this it crashes")
if header.len + size <= header.cap {
header.len += size
break
@ -59,21 +58,20 @@ Arena := struct {
$alloc_zeroed := fn(self: ^Self, $T: type, count: uint): ?[]T {
slice := self.alloc(T, count)
if slice == null return null
mem.set(slice.?.ptr, 0, slice.?.len)
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, mem.size(T, count_new))
slice := self.alloc(T, count_new)
if slice == null return null
mem.copy(@bit_cast(slice.?.ptr), @bit_cast(prev.ptr), mem.size(T, prev.len))
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 {
// lily.log.error("fixme: double free arena. can't fix due to compiler.")
die
return
}
allocation: ^AllocationHeader = @bit_cast(self.allocation)
loop {

View file

@ -2,31 +2,29 @@
fmt_int := fn(buf: []u8, v: @Any(), radix: @TypeOf(v)): uint {
if radix == 0 {
mem.copy(buf.ptr, @bit_cast(&v), @size_of(@TypeOf(v)))
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
// 0x2D == '-'
buf[0] = 0x2D
buf[0] = '-'
prefix_len += 1
}
if radix == 16 {
mem.copy(buf.ptr + prefix_len, "0x".ptr, 2)
mem.copy(buf[prefix_len..], "0x")
prefix_len += 2
} else if radix == 8 {
mem.copy(buf.ptr + prefix_len, "0o".ptr, 2)
mem.copy(buf[prefix_len..], "0o")
prefix_len += 2
} else if radix == 2 {
mem.copy(buf.ptr + prefix_len, "0b".ptr, 2)
mem.copy(buf[prefix_len..], "0b")
prefix_len += 2
}
if v == 0 {
// 0x30 == '0'
buf[prefix_len] = 0x30
buf[prefix_len] = '0'
return prefix_len + 1
}
@ -35,11 +33,9 @@ fmt_int := fn(buf: []u8, v: @Any(), radix: @TypeOf(v)): uint {
remainder := v % radix
v /= radix
if remainder > 9 {
// 0x41 == 'A'
buf[i] = @int_cast(remainder - 10 + 0x41)
buf[i] = @int_cast(remainder - 10 + 'A')
} else {
// 0x30 == '0'
buf[i] = @int_cast(remainder + 0x30)
buf[i] = @int_cast(remainder + '0')
}
i += 1
}
@ -49,31 +45,29 @@ fmt_int := fn(buf: []u8, v: @Any(), radix: @TypeOf(v)): uint {
fmt_bool := fn(buf: []u8, v: bool): uint {
if v {
mem.copy(buf.ptr, "true".ptr, 4)
mem.copy(buf, "true")
return 4
} else {
mem.copy(buf.ptr, "false".ptr, 5)
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.ptr, @name_of(@TypeOf(v)).ptr, @name_of(@TypeOf(v)).len)
mem.copy(buf.ptr + @name_of(@TypeOf(v)).len, ".null".ptr, 5)
return @name_of(@TypeOf(v)).len + 5
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.ptr, @name_of(T).ptr, len)
mem.copy(buf.ptr + len, ".(".ptr, 2)
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.ptr + len, ")".ptr, 1)
mem.copy(buf[len..], ")")
return len + 1
}

View file

@ -27,6 +27,12 @@ Iterator := fn(T: type): type return struct {
$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()
@ -34,6 +40,23 @@ Iterator := fn(T: type): type return struct {
_ = 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")
@ -97,3 +120,53 @@ Take := fn($T: type): type return struct {
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

@ -6,6 +6,18 @@ 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;

View file

@ -1,4 +1,4 @@
.{target} := @use("lib.hb")
.{target, config, fmt} := @use("lib.hb")
LogLevel := enum {
.Error;
@ -9,6 +9,9 @@ LogLevel := enum {
}
$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)),
}
@ -19,3 +22,16 @@ $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])
}
}

View file

@ -1,10 +1,9 @@
.{target, iter: .{Iterator, Next}} := @use("lib.hb")
.{target, iter: .{Iterator, Next}, config, log, Type} := @use("lib.hb")
$size := fn($T: type, count: uint): uint {
return @size_of(T) * count
}
// ! (compiler) bug: parameter named 'align' causes panic.
/// safety: assumes align != 0
$forward_align := fn(ptr: ^u8, _align: uint): ^u8 {
return @bit_cast((@bit_cast(ptr) + _align - 1) / _align * _align)
@ -15,16 +14,50 @@ $backward_align := fn(ptr: ^u8, _align: uint): ^u8 {
return @bit_cast(@bit_cast(ptr) / _align * _align)
}
$copy := fn(dest: ^u8, src: ^u8, len: uint): void {
target.memcopy(dest, src, len)
$is_aligned := fn(ptr: ^u8, _align: uint): bool {
return @bit_cast(ptr) % _align == 0
}
$move := fn(dest: ^u8, src: ^u8, len: uint): void {
target.memmove(dest, src, len)
$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))
}
$set := fn(dest: ^u8, src: u8, len: uint): void {
target.memset(dest, src, len)
$as_bytes := fn(v: @Any()): []u8 {
$match Type(@TypeOf(v)).kind() {
.Pointer => return @as(^u8, @bit_cast(v))[..@size_of(@ChildOf(@TypeOf(v)))],
.Slice => return @as(^u8, @bit_cast(v.ptr))[..size(
@ChildOf(@TypeOf(v)),
v.len,
)],
_ => @error(@TypeOf(v), " is not a pointer or a slice."),
}
}
$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)
}
equals := fn(lhs: []u8, rhs: []u8): bool {
@ -52,7 +85,7 @@ reverse := fn(slice: []u8): []u8 {
} else return slice
}
$bytes := fn(slice: []u8): Iterator(struct {
$iter := fn(slice: []u8): Iterator(struct {
.slice: []u8
$next := fn(self: ^@CurrentScope()): Next(u8) {