holey-bytes/lang/README.md
2024-11-03 15:03:59 +01:00

19 KiB

HERE SHALL THE DOCUMENTATION RESIDE

Enforced Political Views

  • worse is better
  • less is more
  • embrace unsafe {}
  • adhere macro_rules!
  • pessimization == death (put in std::pin::Pin and left with hungry crabs)
  • importing external dependencies == death (fn(dependencies) -> ExecutionStrategy)
  • above sell not be disputed, discussed, or questioned

What hblang is

Holey-Bytes-Language (hblang for short) (*.hb) is the only true language targeting hbvm byte code. hblang is low level, manually managed, and procedural. Its rumored to be better then writing hbasm and you should probably use it for complex applications.

What hblang isnt't

hblang knows what it isn't, because it knows what it is, hblang computes this by sub...

Examples

Examples are also used in tests. To add an example that runs during testing add:

#### <name>
```hb
<example>
```

and also:

<name>;

to the run_tests macro at the bottom of the src/son.rs.

Tour Examples

Following examples incrementally introduce language features and syntax.

main_fn

main := fn(): uint {
	return 1
}

arithmetic

main := fn(): uint {
	return 10 - 20 / 2 + 4 * (2 + 2) - 4 * 4 + (1 << 0) + -1
}

floating_point_arithmetic

main := fn(): f32 {
	return 10. - 20. / 2. + 4. * (2. + 2.) - 4. * 4. + -1.
}

functions

main := fn(): uint {
	return add_one(10) + add_two(20)
}

add_two := fn(x: uint): uint {
	return x + 2
}

add_one := fn(x: uint): uint {
	return x + 1
}

comments

// commant is an item
main := fn(): uint {
	// comment is a statement

	foo(/* comment is an exprression /* if you are crazy */ */)
	return 0
}

foo := fn(comment: void): void return /* comment evaluates to void */

// comments might be formatted in the future

if_statements

main := fn(): uint {
	return fib(10)
}

fib := fn(x: uint): uint {
	if x <= 2 {
		return 1
	} else {
		return fib(x - 1) + fib(x - 2)
	}
}

variables

main := fn(): uint {
	ඞ := 1
	b := 2
	ඞ += 1
	return ඞ - b
}

loops

main := fn(): uint {
	return fib(10)
}

fib := fn(n: uint): uint {
	a := 0
	b := 1
	loop if n == 0 break else {
		c := a + b
		a = b
		b = c
		n -= 1
	}
	return a
}

pointers

main := fn(): uint {
	n := @as(^uint, null)
	if n != null return 9001

	a := 1
	b := &a

	boundary := 1000

	b = b + boundary - 2
	b = b - (boundary - 2)

	modify(b)
	drop(a)
	return *b - 2
}

modify := fn(a: ^uint): void {
	*a = 2
	return
}

drop := fn(a: uint): void {
	return
}

structs

Ty := struct {
	// comment

	a: uint,
}

Ty2 := struct {
	ty: Ty,
	c: uint,
}

useless := struct {}

main := fn(): uint {
	// `packed` structs have no padding (all fields are alighred to 1)
	if @sizeof(packed struct {a: u8, b: u16}) != 3 {
		return 9001
	}

	finst := Ty2.{ty: .{a: 4}, c: 3}
	inst := odher_pass(finst)
	if inst.c == 3 {
		return pass(&inst.ty)
	}
	return 0
}

pass := fn(t: ^Ty): uint {
	return t.a
}

odher_pass := fn(t: Ty2): Ty2 {
	return t
}

hex_octal_binary_literals

main := fn(): uint {
	hex := 0xFF
	decimal := 255
	octal := 0o377
	binary := 0b11111111

	if hex == decimal & octal == decimal & binary == decimal {
		return 0
	}
	return 1
}

struct_operators

Point := struct {
	x: uint,
	y: uint,
}

Rect := struct {
	a: Point,
	b: Point,
}

Color := packed struct {b: u8, g: u8, r: u8, a: u8}

main := fn(): uint {
	i := Color.(0, 0, 0, 0)
	i += .(1, 1, 1, 1)
	if i.r + i.g + i.b + i.a != 4 {
		return 1008
	}

	a := Point.(1, 2)
	b := Point.(3, 4)

	d := Rect.(a + b, b - a)
	zp := Point.(0, 0)
	d2 := Rect.(zp - b, a)
	d2 += d

	c := d2.a + d2.b
	return c.x + c.y
}

global_variables

global_var := 10

complex_global_var := fib(global_var) - 5

fib := fn(n: uint): uint {
	if 2 > n {
		return n
	}
	return fib(n - 1) + fib(n - 2)
}

main := fn(): uint {
	complex_global_var += 5
	return complex_global_var
}

note: values of global variables are evaluated at compile time

directives

foo := @use("foo.hb")

main := fn(): uint {
	byte := @as(u8, 10)
	same_type_as_byte := @as(@TypeOf(byte), 30)
	wide_uint := @as(u32, 40)
	truncated_uint := @as(u8, @intcast(wide_uint))
	widened_float := @as(f64, @floatcast(1.))
	int_from_float := @as(int, @fti(1.))
	float_from_int := @as(f64, @itf(@as(int, 1)))
	size_of_Type_in_bytes := @sizeof(foo.Type)
	align_of_Type_in_bytes := @alignof(foo.Type)
	hardcoded_pointer := @as(^u8, @bitcast(10))
	ecall_that_returns_uint := @as(uint, @eca(1, foo.Type.(10, 20), 5, 6))
	embedded_array := @as([u8; 15], @embed("text.txt"))
	return @inline(foo.foo)
}

// in module: foo.hb

Type := struct {
	brah: uint,
	blah: uint,
}

foo := fn(): uint return 0

// in module: text.txt
arbitrary text
  • @use(<string>): imports a module based on relative path, cycles are allowed when importing
  • @TypeOf(<expr>): results into literal type of whatever the type of <expr> is, <expr> is not included in final binary
  • @as(<ty>, <expr>): hint to the compiler that @TypeOf(<expr>) == <ty>
  • @intcast(<expr>): needs to be used when conversion of @TypeOf(<expr>) would loose precision (widening of integers is implicit)
  • @sizeof(<ty>), @alignof(<ty>): get size and align of a type in bytes
  • @bitcast(<expr>): tell compiler to assume @TypeOf(<expr>) is whatever is inferred, so long as size matches
  • @eca(...<expr>): invoke eca instruction, where return type is inferred and <expr>... are arguments passed to the call in the standard call convention
  • @embed(<string>): include relative file as an array of bytes
  • @inline(<func>, ...<args>): equivalent to <func>(...<args>) but function is guaranteed to inline, compiler will otherwise never inline

c_strings

str_len := fn(str: ^u8): uint {
	len := 0
	loop if *str == 0 break else {
		len += 1
		str += 1
	}
	return len
}

main := fn(): uint {
	// when string ends with '\0' its a C string and thus type is '^u8'
	some_str := "abඞ\n\r\t\{35}\{36373839}\0"
	len := str_len(some_str)
	some_other_str := "fff\0"
	lep := str_len(some_other_str)
	return lep + len
}

struct_patterns

.{fib, fib_iter, Fiber} := @use("fibs.hb")

main := fn(): uint {
	.{a, b} := Fiber.{a: 10, b: 10}
	return fib(a) - fib_iter(b)
}

// in module: fibs.hb

Fiber := struct {a: u8, b: u8}

fib := fn(n: uint): uint if n < 2 {
	return n
} else {
	return fib(n - 1) + fib(n - 2)
}

fib_iter := fn(n: uint): uint {
	a := 0
	b := 1
	loop if n == 0 break else {
		c := a + b
		a = b
		b = c
		n -= 1
	}
	return a
}

arrays

main := fn(): uint {
	addr := @as(u16, 0x1FF)
	msg := [u8].(0, 0, @intcast(addr), @intcast(addr >> 8))
	_force_stack := &msg

	arr := [uint].(1, 2, 4)
	return pass(&arr) + msg[3]
}

pass := fn(arr: ^[uint; 3]): uint {
	return arr[0] + arr[1] + arr[arr[1]]
}

inline

main := fn(): uint {
	return @inline(foo, 1, 2, 3) - 6
}

gb := 0

foo := fn(a: uint, b: uint, c: uint): uint {
	if false | gb != 0 return 1
	return a + b + c
}

idk

_edge_case := @as(uint, idk)

main := fn(): uint {
	big_array := @as([u8; 128], idk)
	i := 0
	loop if i >= 128 break else {
		big_array[i] = 69
		i += 1
	}
	return big_array[42]
}

note: this does not work on scalar values

generic_functions

add := fn($T: type, a: T, b: T): T return a + b

main := fn(): uint {
	return add(u32, 2, 2) - add(uint, 1, 3)
}

Incomplete Examples

comptime_pointers

main := fn(): uint {
	$integer := 7
	modify(&integer)
	return integer
}

modify := fn($num: ^uint): void {
	$: *num = 0
}

generic_types

MALLOC_SYS_CALL := 69
FREE_SYS_CALL := 96

malloc := fn(size: uint, align: uint): ^void return @eca(MALLOC_SYS_CALL, size, align)
free := fn(ptr: ^void, size: uint, align: uint): void return @eca(FREE_SYS_CALL, ptr, size, align)

Vec := fn($Elem: type): type {
	return struct {
		data: ^Elem,
		len: uint,
		cap: uint,
	}
}

new := fn($Elem: type): Vec(Elem) return Vec(Elem).{data: @bitcast(0), len: 0, cap: 0}

deinit := fn($Elem: type, vec: ^Vec(Elem)): void {
	free(@bitcast(vec.data), vec.cap * @sizeof(Elem), @alignof(Elem));
	*vec = new(Elem)
	return
}

push := fn($Elem: type, vec: ^Vec(Elem), value: Elem): ^Elem {
	if vec.len == vec.cap {
		if vec.cap == 0 {
			vec.cap = 1
		} else {
			vec.cap *= 2
		}

		new_alloc := @as(^Elem, @bitcast(malloc(vec.cap * @sizeof(Elem), @alignof(Elem))))
		if new_alloc == 0 return @bitcast(0)

		src_cursor := vec.data
		dst_cursor := new_alloc
		end := vec.data + vec.len

		loop if src_cursor == end break else {
			*dst_cursor = *src_cursor
			src_cursor += 1
			dst_cursor += 1
		}

		if vec.len != 0 {
			free(@bitcast(vec.data), vec.len * @sizeof(Elem), @alignof(Elem))
		}
		vec.data = new_alloc
	}

	slot := vec.data + vec.len;
	*slot = value
	vec.len += 1
	return slot
}

main := fn(): uint {
	vec := new(uint)
	_f := push(uint, &vec, 69)
	res := *vec.data
	deinit(uint, &vec)
	return res
}

fb_driver

arm_fb_ptr := fn(): uint return 100
x86_fb_ptr := fn(): uint return 100

check_platform := fn(): uint {
	return x86_fb_ptr()
}

set_pixel := fn(x: uint, y: uint, width: uint): uint {
	return y * width + x
}

main := fn(): uint {
	fb_ptr := check_platform()
	width := 100
	height := 30
	x := 0
	y := 0
	//t := 0
	i := 0

	loop {
		if x < height {
			//t += set_pixel(x, y, height)
			x += 1
			i += 1
		} else {
			x = 0
			y += 1
			if set_pixel(x, y, height) != i return 0
			if y == width break
		}
	}
	return i
}

Purely Testing Examples

reading_idk

main := fn(): int {
	a := @as(int, idk)
	return a
}

nonexistent_ident_import

main := @use("foo.hb").main
// in module: foo.hb
foo := fn(): void {
	return
}
foo := fn(): void {
	return
}
main := @use("bar.hb").mian
// in module: bar.hb
main := fn(): void {
	return
}

big_array_crash

SIN_TABLE := [int].(0, 174, 348, 523, 697, 871, 1045, 1218, 1391, 1564, 1736, 1908, 2079, 2249, 2419, 2588, 2756, 2923, 3090, 3255, 3420, 3583, 3746, 3907, 4067, 4226, 4384, 4540, 4695, 4848, 5000, 5150, 5299, 5446, 5591, 5735, 5877, 6018, 6156, 6293, 6427, 6560, 6691, 6819, 6946, 7071, 7193, 7313, 7431, 7547, 7660, 7771, 7880, 7986, 8090, 8191, 8290, 8386, 8480, 8571, 8660, 8746, 8829, 8910, 8987, 9063, 9135, 9205, 9271, 9335, 9396, 9455, 9510, 9563, 9612, 9659, 9702, 9743, 9781, 9816, 9848, 9877, 9902, 9925, 9945, 9961, 9975, 9986, 9993, 9998, 10000)

main := fn(): int return SIN_TABLE[10]

returning_global_struct

Color := struct {r: u8, g: u8, b: u8, a: u8}
white := Color.(255, 255, 255, 255)
random_color := fn(): Color {
	return white
}
main := fn(): uint {
	val := random_color()
	return @as(uint, val.r) + val.g + val.b + val.a
}

small_struct_bitcast

Color := struct {r: u8, g: u8, b: u8, a: u8}
white := Color.(255, 255, 255, 255)
u32_to_color := fn(v: u32): Color return @as(Color, @bitcast(u32_to_u32(@bitcast(v))))
u32_to_u32 := fn(v: u32): u32 return v
main := fn(): uint {
	return u32_to_color(@bitcast(white)).r
}

small_struct_assignment

Color := struct {r: u8, g: u8, b: u8, a: u8}
white := Color.(255, 255, 255, 255)
black := Color.(0, 0, 0, 0)
main := fn(): uint {
	f := black
	f = white
	return f.a
}

intcast_store

SetMsg := packed struct {a: u8, count: u32, size: u32, src: ^u8, dest: ^u8}
set := fn($Expr: type, src: ^Expr, dest: ^Expr, count: uint): u32 {
	l := SetMsg.(5, @intcast(count), @intcast(@sizeof(Expr)), @bitcast(src), @bitcast(dest))
	return l.count
}

main := fn(): uint {
	return set(uint, &0, &0, 1024)
}

string_flip

U := struct {u: uint}
main := fn(): uint {
	arr := @as([U; 2 * 2], idk)

	i := 0
	loop if i == 2 * 2 break else {
		arr[i] = .(i)
		i += 1
	}

	i = 0
	loop if i == 2 / 2 break else {
		j := 0
		loop if j == 2 break else {
			a := i * 2 + j
			b := (2 - i - 1) * 2 + j
			tmp := arr[a]
			arr[a] = arr[b]
			arr[b] = tmp
			j += 1
		}
		i += 1
	}

	return arr[0].u
}

wide_ret

OemIdent := struct {
	dos_version: [u8; 8],
	dos_version_name: [u8; 8],
}

Stru := struct {
	a: u16,
	b: u16,
}

small_struct := fn(): Stru {
	return .{a: 0, b: 0}
}

maina := fn(major: uint, minor: uint): OemIdent {
	_f := small_struct()
	ver := [u8].(0, 0, 0, 3, 1, 0, 0, 0)
	return OemIdent.(ver, ver)
}

main := fn(): uint {
	m := maina(0, 0)
	return m.dos_version[3] - m.dos_version_name[4]
}

comptime_min_reg_leak

a := @use("math.hb").min(100, 50)

main := fn(): uint {
	return a
}

// in module: math.hb

SIZEOF_uint := 32
SHIFT := SIZEOF_uint - 1
min := fn(a: uint, b: uint): uint {
	c := a - b
	return b + (c & c >> SHIFT)
}

different_types

Color := struct {
	r: u8,
	g: u8,
	b: u8,
	a: u8,
}

Point := struct {
	x: u32,
	y: u32,
}

Pixel := struct {
	color: Color,
	pouint: Point,
}

main := fn(): uint {
	pixel := Pixel.{
		color: Color.{
			r: 255,
			g: 0,
			b: 0,
			a: 255,
		},
		pouint: Point.{
			x: 0,
			y: 2,
		},
	}

	soupan := 1
	if *(&pixel.pouint.x + soupan) != 2 {
		return 0
	}

	if *(&pixel.pouint.y - 1) != 0 {
		return 64
	}

	return pixel.pouint.x + pixel.pouint.y + pixel.color.r
		+ pixel.color.g + pixel.color.b + pixel.color.a
}

struct_return_from_module_function

bar := @use("bar.hb")

main := fn(): uint {
	return 7 - bar.foo().x - bar.foo().y - bar.foo().z
}

// in module: bar.hb


foo := fn(): Foo {
	return .{x: 3, y: 2, z: 2}
}

Foo := struct {x: uint, y: u32, z: u32}

sort_something_viredly

main := fn(): uint {
	return sqrt(100)
}

sqrt := fn(x: uint): uint {
	temp := 0
	g := 0
	b := 32768
	bshift := 15
	loop if b == 0 {
		break
	} else {
		bshift -= 1
		temp = b + (g << 1)
		temp <<= bshift
		if x >= temp {
			g += b
			x -= temp
		}
		b >>= 1
	}
	return g
}

struct_in_register

ColorBGRA := struct {b: u8, g: u8, r: u8, a: u8}
MAGENTA := ColorBGRA.{b: 205, g: 0, r: 205, a: 255}

main := fn(): uint {
	color := MAGENTA
	return color.r
}

comptime_function_from_another_file

stn := @use("stn.hb")

CONST_A := 100
CONST_B := 50
a := stn.math.min(CONST_A, CONST_B)

main := fn(): uint {
	return a
}

// in module: stn.hb
math := @use("math.hb")

// in module: math.hb
SIZEOF_uint := 32
SHIFT := SIZEOF_uint - 1
min := fn(a: uint, b: uint): uint {
	c := a - b
	return b + (c & c >> SHIFT)
}

inline_test

fna := fn(a: uint, b: uint, c: uint): uint return a + b + c

scalar_values := fn(): uint {
	return @inline(fna, 1, @inline(fna, 2, 3, 4), -10)
}

A := struct {a: uint}
AB := struct {a: A, b: uint}

mangle := fn(a: A, ab: AB): uint {
	return a.a + ab.a.a - ab.b
}

structs := fn(): uint {
	return @inline(mangle, .(0), .(.(@inline(mangle, .(20), .(.(5), 5))), 20))
}

main := fn(): uint {
	if scalar_values() != 0 return 1
	if structs() != 0 return structs()

	return 0
}

inlined_generic_functions

abs := fn($Expr: type, x: Expr): Expr {
	mask := x >> @bitcast(@sizeof(Expr) - 1)
	return (x ^ mask) - mask
}

main := fn(): uint {
	return @inline(abs, uint, -10)
}

some_generic_code

some_func := fn($Elem: type): void {
	return
}

main := fn(): void {
	some_func(u8)
	return
}

integer_inference_issues

.{integer_range} := @use("random.hb")
main := fn(): void {
	a := integer_range(0, 1000)
	return
}

// in module: random.hb
integer_range := fn(min: uint, max: uint): uint {
	return @eca(3, 4) % (@bitcast(max) - min + 1) + min
}

signed_to_unsigned_upcast

main := fn(): uint return @as(i32, 1)

writing_into_string

outl := fn(): void {
	msg := "whahaha\0"
	_u := @as(u8, 0)
	return
}

inl := fn(): void {
	msg := "luhahah\0"
	return
}

main := fn(): void {
	outl()
	inl()
	return
}

request_page

request_page := fn(page_count: u8): ^u8 {
	msg := "\{00}\{01}xxxxxxxx\0"
	msg_page_count := msg + 1;
	*msg_page_count = page_count
	return @eca(3, 2, msg, 12)
}

create_back_buffer := fn(total_pages: int): ^u32 {
	if total_pages <= 0xFF {
		return @bitcast(request_page(@intcast(total_pages)))
	}
	ptr := request_page(255)
	remaining := total_pages - 0xFF
	loop if remaining <= 0 break else {
		if remaining < 0xFF {
			_f := request_page(@intcast(remaining))
		} else {
			_f := request_page(0xFF)
		}
		remaining -= 0xFF
	}
	return @bitcast(ptr)
}

main := fn(): void {
	_f := create_back_buffer(400)
	return
}

tests_ptr_to_ptr_copy

main := fn(): uint {
	back_buffer := @as([u8; 1024 * 10], idk)

	n := 0
	loop if n >= 1024 break else {
		back_buffer[n] = 64
		n += 1
	}
	n = 1
	loop if n >= 10 break else {
		*(@as(^[u8; 1024], @bitcast(&back_buffer)) + n) = *@as(^[u8; 1024], @bitcast(&back_buffer))
		n += 1
	}
	return back_buffer[1024 * 2]
}

null_check_test

main := fn(): unit {
	ptr := @as(?^uint, null)
	*ptr = 0

	if ptr == null {
		return 1
	}
	
	return *ptr
}

Just Testing Optimizations

const_folding_with_arg

main := fn(arg: uint): uint {
	// reduces to 0
	return arg + 0 - arg * 1 + arg + 1 + arg + 2 + arg + 3 - arg * 3 - 6
}

branch_assignments

main := fn(arg: uint): uint {
	if arg == 1 {
		arg = 1
	} else if arg == 0 {
		arg = 2
	} else {
		arg = 3
	}
	return arg
}

exhaustive_loop_testing

main := fn(): uint {
	loop break

	x := 0
	loop {
		x += 1
		break
	}

	if multiple_breaks(0) != 3 {
		return 1
	}

	if multiple_breaks(4) != 10 {
		return 2
	}

	if state_change_in_break(0) != 0 {
		return 3
	}

	if state_change_in_break(4) != 10 {
		return 4
	}

	if continue_and_state_change(10) != 10 {
		return 5
	}

	if continue_and_state_change(3) != 0 {
		return 6
	}

	infinite_loop()
	return 0
}

infinite_loop := fn(): void {
	f := 0
	loop {
		if f == 1 {
			f = 0
		} else {
			f = 1
		}

		f = continue_and_state_change(0)
	}
}

multiple_breaks := fn(arg: uint): uint {
	loop if arg < 10 {
		arg += 1
		if arg == 3 break
	} else break
	return arg
}

state_change_in_break := fn(arg: uint): uint {
	loop if arg < 10 {
		if arg == 3 {
			arg = 0
			break
		}
		arg += 1
	} else break
	return arg
}

continue_and_state_change := fn(arg: uint): uint {
	loop if arg < 10 {
		if arg == 2 {
			arg = 4
			continue
		}
		if arg == 3 {
			arg = 0
			break
		}
		arg += 1
	} else break
	return arg
}

pointer_opts

main := fn(): uint {
	mem := &0;
	*mem = 1;
	*mem = 2

	b := *mem + *mem
	clobber(mem)

	b -= *mem
	return b
}

clobber := fn(cb: ^uint): void {
	*cb = 4
	return
}

conditional_stores

main := fn(): uint {
	cnd := cond()
	mem := &1

	if cnd == 0 {
		*mem = 0
	} else {
		*mem = 2
	}

	return *mem
}

cond := fn(): uint return 0

loop_stores

main := fn(): uint {
	mem := &10

	loop if *mem == 0 break else {
		*mem -= 1
	}

	return *mem
}

dead_code_in_loop

main := fn(): uint {
	n := 0

	loop if n < 10 {
		if n < 10 break
		n += 1
	} else break

	loop if n == 0 return n

	return 1
}

infinite_loop_after_peephole

main := fn(): uint {
	n := 0
	f := 0
	loop if n != 0 break else {
		f += 1
	}
	return f
}

aliasing_overoptimization

Foo := struct {ptr: ^uint, rnd: uint}

main := fn(): uint {
	mem := &2
	stru := Foo.(mem, 0);
	*stru.ptr = 0
	return *mem
}

global_aliasing_overptimization

var := 0

main := fn(): uint {
	var = 2
	clobber()
	return var
}

clobber := fn(): void {
	var = 0
}

overwrite_aliasing_overoptimization

Foo := struct {a: int, b: int}
Bar := struct {f: Foo, b: int}

main := fn(): int {
	value := Bar.{b: 1, f: .(4, 1)}
	value.f = opaque()
	return value.f.a - value.f.b - value.b
}

opaque := fn(): Foo {
	return .(3, 2)
}