parse const decls

This commit is contained in:
2026-07-25 00:26:47 +02:00
parent c7f527e3c3
commit 90c7195d4b
28 changed files with 1452 additions and 1134 deletions
+1
View File
@@ -0,0 +1 @@
# todo: move ast stuff from parser module to here
+127 -57
View File
@@ -1,29 +1,52 @@
import "@std" import "@std"
import "@std/mem" import "@std/mem"
import "@std/arraylist"
import "@std/strmap" import "@std/strmap"
import "@std/enums/enummap"
import "@std/arraylist"
import "@std/debug"
import "@source/strpool" import "@source/strpool"
ErrorCode :: enum {
invalid_character,
float_must_end_with_digit,
}
ErrorDetails :: struct {
name []u8
message []u8
}
error_msg_map std.EnumMap(ErrorCode, ErrorDetails) :: enummap.init({
invalid_character = ErrorDetails {
name = "L0",
message = "invalid character",
},
float_must_end_with_digit = ErrorDetails {
name = "L1",
message = "float must end with a digit",
},
})
keywords std.StringMap(TokenKind) :: strmap.init([ keywords std.StringMap(TokenKind) :: strmap.init([
{ "proc", .proc }, { "proc", .proc },
{ "return", .return }, { "return", .return },
{ "if", .if }, { "if", .if },
{ "for", .for }, { "for", .for },
{ "else", .else }, { "else", .else },
{ "while", .while }, { "while", .while },
]) ])
TokenIndex :: alias usize TokenId :: distinct u32
ScanDiagnostic :: struct { Diagnostic :: struct {
token TokenIndex code ErrorCode
message []u8 token TokenId
} }
State :: struct { State :: struct {
tokens std.ArrayList(Token) tokens std.ArrayList(Token)
diagnostics std.ArrayList(ScanDiagnostic) diagnostics std.ArrayList(Diagnostic)
} }
init proc(allocator mem.Allocator) State { init proc(allocator mem.Allocator) State {
@@ -39,8 +62,8 @@ deinit proc(state @mut State) void {
} }
scan proc(state @mut State, input []u8) void ! (mem.AllocError | strpool.InternError) { scan proc(state @mut State, input []u8) void ! (mem.AllocError | strpool.InternError) {
tokens :: &state.tokens tokens :: &state.tokens
diagnostics :: &state.diagnostics diagnostics :: &state.diagnostics
cursor usize = 0 cursor usize = 0
while cursor < input.len { while cursor < input.len {
@@ -48,9 +71,9 @@ scan proc(state @mut State, input []u8) void ! (mem.AllocError | strpool.InternE
# whitespace # whitespace
if char == '\n' { if char == '\n' {
try arraylist.append(tokens, Token{ kind = .newline, start = cursor }) try add_token(tokens, Token{ kind = .newline, start = cursor })
cursor += 1 cursor += 1
continue continue
} else if is_whitespace(char) { } else if is_whitespace(char) {
cursor += 1 cursor += 1
continue continue
@@ -58,10 +81,8 @@ scan proc(state @mut State, input []u8) void ! (mem.AllocError | strpool.InternE
# comments # comments
if char == '#' { if char == '#' {
# fixme(brolang): while currently requires curly braces but this should be legal while (cursor < input.len and input[cursor] != '\n') cursor += 1
#while (cursor < input.len and input[cursor] != '\n') cursor += 1 cursor += 1 # also skip newline
while cursor < input.len and input[cursor] != '\n' : cursor += 1 {}
continue continue
} }
@@ -75,7 +96,7 @@ scan proc(state @mut State, input []u8) void ! (mem.AllocError | strpool.InternE
is_alpha(input[cursor]) or is_alpha(input[cursor]) or
is_digit(input[cursor]) or is_digit(input[cursor]) or
input[cursor] == '_' input[cursor] == '_'
)) : cursor += 1 {} )) cursor += 1
kind :: strmap.get(&keywords, input[start..cursor]) orelse .ident kind :: strmap.get(&keywords, input[start..cursor]) orelse .ident
@@ -83,9 +104,9 @@ scan proc(state @mut State, input []u8) void ! (mem.AllocError | strpool.InternE
str_id :: if (kind == .ident) str_id :: if (kind == .ident)
try strpool.intern(&strpool.strings, input[start..cursor]) try strpool.intern(&strpool.strings, input[start..cursor])
else else
strpool.NoId strpool.NO_ID
try arraylist.append(tokens, Token{ try add_token(tokens, Token{
kind = kind, kind = kind,
start = start, start = start,
str_id = str_id str_id = str_id
@@ -99,7 +120,7 @@ scan proc(state @mut State, input []u8) void ! (mem.AllocError | strpool.InternE
has_decimal bool = false has_decimal bool = false
# scan integer part # scan integer part
while cursor < input.len and is_digit(input[cursor]) : cursor += 1 {} while (cursor < input.len and is_digit(input[cursor])) cursor += 1
# check for decimal point # check for decimal point
if cursor < input.len and input[cursor] == '.' { if cursor < input.len and input[cursor] == '.' {
@@ -109,22 +130,17 @@ scan proc(state @mut State, input []u8) void ! (mem.AllocError | strpool.InternE
# assert that decimals follow the decimal point # assert that decimals follow the decimal point
if has_decimal and (cursor >= input.len or !is_digit(input[cursor])) { if has_decimal and (cursor >= input.len or !is_digit(input[cursor])) {
token :: tokens.items.len token :: token_id(tokens.items.len)
try arraylist.append(tokens, Token{ kind = .invalid, start = start }) try arraylist.append(diagnostics, Diagnostic{ token = token, code = .float_must_end_with_digit })
try arraylist.append(diagnostics, ScanDiagnostic{ try add_token(tokens, Token{ kind = .invalid, start = start })
token = token, continue
message = "float must end with a digit",
})
continue
} }
# scan decimal part # scan decimal part
while cursor < input.len and is_digit(input[cursor]) : cursor += 1 {} while (cursor < input.len and is_digit(input[cursor])) cursor += 1
if (has_decimal) kind :: if (has_decimal) .float else .int
try arraylist.append(tokens, Token{ kind = .float, start = start }) try add_token(tokens, Token{ kind = kind, start = start })
else
try arraylist.append(tokens, Token{ kind = .int, start = start })
continue continue
} }
@@ -135,15 +151,15 @@ scan proc(state @mut State, input []u8) void ! (mem.AllocError | strpool.InternE
cursor += 1 cursor += 1
while cursor < input.len and input[cursor] != '"' and input[cursor] != '\n' : cursor += 1 { while cursor < input.len and input[cursor] != '"' and input[cursor] != '\n' : cursor += 1 {
# ignore escaped characters # ignore escaped characters
if (input[cursor] == '\\' and cursor + 1 < input.len) cursor += 1 if (input[cursor] == '\\' and cursor + 1 < input.len) cursor += 1
} }
if cursor < input.len and input[cursor] == '"' { if cursor < input.len and input[cursor] == '"' {
cursor += 1 cursor += 1
try arraylist.append(tokens, Token{ kind = .string, start = start }) try add_token(tokens, Token{ kind = .string, start = start })
} else { } else {
try arraylist.append(tokens, Token{ kind = .invalid, start = start }) try add_token(tokens, Token{ kind = .invalid, start = start })
} }
continue continue
@@ -151,51 +167,95 @@ scan proc(state @mut State, input []u8) void ! (mem.AllocError | strpool.InternE
# mutable assignment # mutable assignment
if char == '=' { if char == '=' {
try arraylist.append(tokens, Token{ kind = .equal, start = cursor }) try add_token(tokens, Token{ kind = .equal, start = cursor })
cursor += 1 cursor += 1
continue continue
} }
# immutable assignment # immutable assignment or single colon
if cursor + 1 < input.len and char == ':' and input[cursor + 1] == ':' { if char == ':' {
try arraylist.append(tokens, Token{ kind = .double_colon, start = cursor }) if cursor + 1 < input.len and input[cursor + 1] == ':' {
cursor += 2 try add_token(tokens, Token{ kind = .double_colon, start = cursor })
cursor += 2
continue
}
try add_token(tokens, Token{ kind = .colon, start = cursor })
cursor += 1
continue continue
} }
# parentheses # parentheses
if char == '(' { if char == '(' {
try arraylist.append(tokens, Token{ kind = .open_paren, start = cursor }) try add_token(tokens, Token{ kind = .open_paren, start = cursor })
cursor += 1 cursor += 1
continue continue
} else if char == ')' { } else if char == ')' {
try arraylist.append(tokens, Token{ kind = .close_paren, start = cursor }) try add_token(tokens, Token{ kind = .close_paren, start = cursor })
cursor += 1 cursor += 1
continue continue
} }
# curly braces # curly braces
if char == '{' { if char == '{' {
try arraylist.append(tokens, Token{ kind = .open_curly, start = cursor }) try add_token(tokens, Token{ kind = .open_curly, start = cursor })
cursor += 1 cursor += 1
continue continue
} else if char == '}' { } else if char == '}' {
try arraylist.append(tokens, Token{ kind = .close_curly, start = cursor }) try add_token(tokens, Token{ kind = .close_curly, start = cursor })
cursor += 1 cursor += 1
continue continue
} }
# invalid character # invalid character
token :: tokens.items.len token :: token_id(tokens.items.len)
try arraylist.append(tokens, Token{ kind = .invalid, start = cursor }) try arraylist.append(diagnostics, Diagnostic{ token = token, code = .invalid_character })
try arraylist.append(diagnostics, ScanDiagnostic{ try add_token(tokens, Token{ kind = .invalid, start = cursor })
token = token,
message = "invalid character",
})
cursor += 1 cursor += 1
} }
try arraylist.append(tokens, Token{ kind = .eof, start = cursor }) try add_token(tokens, Token{ kind = .eof, start = cursor })
}
#! returns the cursor position after scanning a number.
scan_number proc(start usize, input []u8) usize {
debug.assert(is_digit(input[start]))
has_decimal bool = false
cursor usize = start
# scan integer part
while cursor < input.len and is_digit(input[cursor]) : cursor += 1 {}
# check for decimal point
if cursor < input.len and input[cursor] == '.' {
has_decimal = true
cursor += 1
}
# assert that decimals follow the decimal point
debug.assert(!(has_decimal and (cursor >= input.len or !is_digit(input[cursor]))))
# scan decimal part
while cursor < input.len and is_digit(input[cursor]) : cursor += 1 {}
return cursor
}
#! returns the cursor position after scanning a string.
scan_string proc(start usize, input []u8) usize {
debug.assert(input[start] == '"')
cursor usize = start + 1
while cursor < input.len and input[cursor] != '"' and input[cursor] != '\n' : cursor += 1 {
# ignore escaped characters
if (input[cursor] == '\\' and cursor + 1 < input.len) cursor += 1
}
debug.assert(cursor < input.len and input[cursor] == '"')
cursor += 1
return cursor
} }
hide is_whitespace proc(char u8) bool { hide is_whitespace proc(char u8) bool {
@@ -215,3 +275,13 @@ hide is_digit proc(char u8) bool {
else: false else: false
} }
} }
hide add_token proc(tokens @mut std.ArrayList(Token), token Token) void ! mem.AllocError {
_ = token_id(tokens.items.len)
try arraylist.append(tokens, token)
}
hide token_id proc(idx uint) TokenId {
debug.assert(u64(idx) < u64(maxval!(TokenId)))
return TokenId(idx)
}
+13 -14
View File
@@ -3,20 +3,19 @@ import "@std/mem"
import "@std/testing" import "@std/testing"
handles_keywords_identifiers_and_error_progress test { handles_keywords_identifiers_and_error_progress test {
strpool.strings = strpool.init(mem.c_allocator) strpool.strings = strpool.init(mem.c_allocator)
defer strpool.deinit(&strpool.strings) defer strpool.deinit(&strpool.strings)
state State = init(mem.c_allocator) state State = init(mem.c_allocator)
defer deinit(&state) defer deinit(&state)
try scan(&state, "if name # comment\n@1.") try scan(&state, "if name # comment\n@1.")
try testing.expect_equal(6, state.tokens.items.len) try testing.expect_equal(5, state.tokens.items.len)
try testing.expect_equal(TokenKind.if, state.tokens.items[0].kind) try testing.expect_equal(TokenKind.if, state.tokens.items[0].kind)
try testing.expect_equal(TokenKind.ident, state.tokens.items[1].kind) try testing.expect_equal(TokenKind.ident, state.tokens.items[1].kind)
try testing.expect_equal(TokenKind.newline, state.tokens.items[2].kind) try testing.expect_equal(TokenKind.invalid, state.tokens.items[2].kind)
try testing.expect_equal(TokenKind.invalid, state.tokens.items[3].kind) try testing.expect_equal(TokenKind.invalid, state.tokens.items[3].kind)
try testing.expect_equal(TokenKind.invalid, state.tokens.items[4].kind) try testing.expect_equal(TokenKind.eof, state.tokens.items[4].kind)
try testing.expect_equal(TokenKind.eof, state.tokens.items[5].kind) try testing.expect_equal(2, state.diagnostics.items.len)
try testing.expect_equal(2, state.diagnostics.items.len) try testing.expect_equal("name", strpool.get_str(&strpool.strings, state.tokens.items[1].str_id)?)
try testing.expect_equal("name", strpool.get_str(&strpool.strings, state.tokens.items[1].str_id)?)
} }
+32 -28
View File
@@ -1,34 +1,38 @@
import "@source/strpool" import "@source/strpool"
TokenKind :: enum {
if
else
for
while
proc
return
ident
int
float
string
equal
double_colon
open_paren
close_paren
open_curly
close_curly
newline
invalid
eof
}
Token :: struct { Token :: struct {
kind TokenKind kind TokenKind
start int start int
str_id strpool.StringId = strpool.NoId str_id strpool.StringId = strpool.NO_ID
}
TokenKind :: enum {
# keywords
if, else
for, while
proc, return
# literals
ident, string
int, float
# comparison
equal_equal, not_equal
less, less_equal
greater, greater_equal
# assignment
equal, double_colon
# delimiters
open_paren, close_paren
open_bracket, close_bracket
open_curly, close_curly
colon
newline
# special
eof
invalid
} }
+42 -17
View File
@@ -1,34 +1,24 @@
import "@std/debug" import "@std/debug"
import "@std/mem" import "@std/mem"
import "@std/enums/enummap"
test import "@std/enums" test import "@std/enums/enummap"
test import "@std/arraylist" test import "@std/arraylist"
test import "@std/hashmap" test import "@std/hashmap"
test import "@std/strmap" test import "@std/strmap"
import "@source/strpool" import "@source/strpool"
import "@source/lexer" import "@source/lexer"
import "@source/parser"
test import "@source/strpool" test import "@source/strpool"
test import "@source/lexer" test import "@source/lexer"
program :: program ::
`# these are immutable `# literals
`x :: 32 `x :: 123
`y :: 3.2
` # todo: consider making current aliased types (IDs) distinct instead
`# these are mutable
`z u32 = 54
`
`# keywords
`if
`else
`for
`while
`proc
`return
`
`main proc() void {}
main proc() void { main proc() void {
strpool.strings = strpool.init(mem.c_allocator) strpool.strings = strpool.init(mem.c_allocator)
@@ -48,5 +38,40 @@ main proc() void {
for scan_state.tokens.items |token| { for scan_state.tokens.items |token| {
debug.print("{}\n", {token.kind}) debug.print("{}\n", {token.kind})
} }
debug.print("]]\n\n", {})
debug.print("TOKENS::DIAGNOSTICS::[[\n", {})
for scan_state.diagnostics.items |diagnostic| {
details :: enummap.get(&lexer.error_msg_map, diagnostic.code)
debug.print("{}: {}\n", {details?.name, details?.message})
}
debug.print("]]\n\n", {})
parse_state parser.State = parser.init(mem.c_allocator)
defer parser.deinit(&parse_state)
parser.parse(&parse_state, scan_state.tokens.items) catch |err| {
debug.print("failed to parse: {}\n", {err})
return
}
debug.print("AST::[[\n", {})
for parse_state.nodes.items |node, id| {
token :: scan_state.tokens.items[node.main_token]
end :: if (token.kind == .int or token.kind == .float)
lexer.scan_number(token.start, program)
else if (token.kind == .string)
lexer.scan_string(token.start, program)
else
token.start
debug.print("{} (id = {}): {}\n", {
node.kind,
id,
program[token.start..end],
#node.data0,
#node.data1,
})
}
debug.print("]]\n", {}) debug.print("]]\n", {})
} }
+156
View File
@@ -0,0 +1,156 @@
import "@std"
import "@std/mem"
import "@std/arraylist"
import "@std/debug"
import "@std/enums/enummap"
import "@source/lexer"
NodeId :: distinct u32
ExtraId :: distinct NodeId
NO_ID :: maxval!(NodeId)
Diagnostic :: struct {
code ErrorCode
node NodeId
}
ParseError :: alias ErrorCode | mem.AllocError
ErrorCode :: enum {
unexpected_token,
}
ErrorDetails :: struct {
name []u8
message []u8
}
error_msg_map std.EnumMap(ErrorCode, ErrorDetails) :: enummap.init({
unexpected_token = ErrorDetails {
name = "P0",
message = "unexpected token",
},
})
hide Token :: alias lexer.Token
hide TokenId :: alias lexer.TokenId
hide TokenKind :: alias lexer.TokenKind
NodeData :: union {
node_id NodeId
extra_id ExtraId
}
Node :: struct {
kind NodeKind
main_token TokenId
data0 NodeData = NodeData{ node_id = NO_ID }
data1 NodeData = NodeData{ node_id = NO_ID }
}
NodeKind :: enum {
literal_int
literal_float
literal_string
expr_unary
expr_binary
decl
invalid
}
State :: struct {
nodes std.ArrayList(Node)
#! array of indexes into the nodes array.
extra std.ArrayList(NodeId)
next_token TokenId = TokenId(0)
}
init proc(allocator mem.Allocator) State {
return State{
nodes = arraylist.init(allocator),
extra = arraylist.init(allocator),
}
}
deinit proc(state @mut State) void {
arraylist.deinit(&state.nodes)
arraylist.deinit(&state.extra)
}
parse proc(state @mut State, tokens []Token) void ! ParseError {
_ = try parse_decl(state, tokens)
}
parse_decl proc(state @mut State, tokens []Token) NodeId ! ParseError {
# expect identifier
try expect(state, tokens, .ident)
ident_token :: state.next_token
state.next_token += 1
# expect `::` (immutable assignment)
try expect(state, tokens, .double_colon)
state.next_token += 1
# expect expression
expr :: try parse_primary(state, tokens)
# expect statement terminator (newline)
try expect_either(state, tokens, &[.newline, .eof])
state.next_token += 1
decl :: try add_node(&state.nodes, Node{
kind = .decl,
main_token = ident_token,
data0 = NodeData{ node_id = expr },
})
return decl
}
parse_primary proc(state @mut State, tokens []Token) NodeId ! mem.AllocError {
start_token :: state.next_token
state.next_token += 1
return match tokens[start_token].kind {
.int: try add_node(&state.nodes, Node{
kind = .literal_int,
main_token = start_token,
})
.float: try add_node(&state.nodes, Node{
kind = .literal_float,
main_token = start_token,
})
.string: try add_node(&state.nodes, Node{
kind = .literal_string,
main_token = start_token,
})
else: try add_node(&state.nodes, Node{
kind = .invalid,
main_token = start_token,
})
}
}
hide expect proc(state @mut State, tokens []Token, token_kind TokenKind) void ! ErrorCode {
if (tokens[state.next_token].kind != token_kind) return .unexpected_token
}
hide expect_either proc(state @mut State, tokens []Token, token_kinds []TokenKind) void ! ErrorCode {
for (token_kinds) |kind| if (tokens[state.next_token].kind == kind) return
return .unexpected_token
}
hide add_node proc(nodes @mut std.ArrayList(Node), node Node) NodeId ! mem.AllocError {
id :: node_id(nodes.items.len)
try arraylist.append(nodes, node)
return id
}
hide node_id proc(idx uint) NodeId {
debug.assert(u64(idx) < u64(NO_ID))
return NodeId(idx)
}
+33 -32
View File
@@ -8,58 +8,59 @@ strings StringPool = undefined
InternError :: enum { out_of_space } InternError :: enum { out_of_space }
StringId :: alias u32 StringId :: alias u32
NoId :: maxval!(StringId)
NO_ID :: maxval!(StringId)
StringPool :: struct { StringPool :: struct {
ids hashmap.StringHashMap(StringId) # string → id ids hashmap.StringHashMap(StringId) # string → id
strings arraylist.ArrayList([]u8) # id → owned string strings arraylist.ArrayList([]u8) # id → owned string
allocator mem.Allocator allocator mem.Allocator
} }
init proc(allocator mem.Allocator) StringPool { init proc(allocator mem.Allocator) StringPool {
return StringPool{ return StringPool{
ids = hashmap.init(allocator), ids = hashmap.init(allocator),
strings = arraylist.init(allocator), strings = arraylist.init(allocator),
allocator = allocator, allocator = allocator,
} }
} }
deinit proc(pool @mut StringPool) void { deinit proc(pool @mut StringPool) void {
hashmap.deinit(&pool.ids) hashmap.deinit(&pool.ids)
for pool.strings.items |str| { for pool.strings.items |str| {
mem.free(pool.allocator, str) mem.free(pool.allocator, str)
} }
arraylist.deinit(&pool.strings) arraylist.deinit(&pool.strings)
} }
intern proc(pool @mut StringPool, str []u8) StringId ! (mem.AllocError | InternError) { intern proc(pool @mut StringPool, str []u8) StringId ! (mem.AllocError | InternError) {
if hashmap.get(&pool.ids, str) |id| return id if hashmap.get(&pool.ids, str) |id| return id
if (pool.strings.items.len >= usize(NoId)) return .out_of_space if (pool.strings.items.len >= usize(NO_ID)) return .out_of_space
id StringId :: StringId(pool.strings.items.len) id StringId :: StringId(pool.strings.items.len)
owned_str []mut u8 :: try mem.alloc(u8, pool.allocator, str.len) owned_str []mut u8 :: try mem.alloc(u8, pool.allocator, str.len)
errdefer mem.free(pool.allocator, owned_str) errdefer mem.free(pool.allocator, owned_str)
memcopy!(owned_str, str) memcopy!(owned_str, str)
try arraylist.append(&pool.strings, owned_str) try arraylist.append(&pool.strings, owned_str)
errdefer _ = arraylist.pop(&pool.strings) errdefer _ = arraylist.pop(&pool.strings)
hashmap.put(&pool.ids, owned_str, id) catch |err| { hashmap.put(&pool.ids, owned_str, id) catch |err| {
match err { match err {
.key_exists: unreachable .key_exists: unreachable
else: return err else: return err
} }
} }
return id return id
} }
get_str proc(pool @StringPool, id StringId) ?[]u8 { get_str proc(pool @StringPool, id StringId) ?[]u8 {
if (usize(id) >= pool.strings.items.len) return null if (usize(id) >= pool.strings.items.len) return null
return pool.strings.items[usize(id)] return pool.strings.items[usize(id)]
} }
get_id proc(pool @StringPool, str []u8) ?StringId { get_id proc(pool @StringPool, str []u8) ?StringId {
return hashmap.get(&pool.ids, str) return hashmap.get(&pool.ids, str)
} }
+16 -16
View File
@@ -2,23 +2,23 @@ import "@std/mem"
import "@std/testing" import "@std/testing"
handles_intern test { handles_intern test {
pool StringPool = init(mem.c_allocator) pool StringPool = init(mem.c_allocator)
defer deinit(&pool) defer deinit(&pool)
input [5]mut u8 = ['h', 'e', 'l', 'l', 'o'] input [5]mut u8 = ['h', 'e', 'l', 'l', 'o']
id :: try intern(&pool, input[..]) id :: try intern(&pool, input[..])
duplicate :: try intern(&pool, "hello") duplicate :: try intern(&pool, "hello")
input[0] = 'j' input[0] = 'j'
same_id :: get_id(&pool, "hello") same_id :: get_id(&pool, "hello")
mutated_id :: get_id(&pool, input[..]) mutated_id :: get_id(&pool, input[..])
hello_str :: get_str(&pool, id) hello_str :: get_str(&pool, id)
invalid_str :: get_str(&pool, id + 1) invalid_str :: get_str(&pool, id + 1)
try testing.expect_equal(0, id) try testing.expect_equal(0, id)
try testing.expect_equal(id, duplicate) try testing.expect_equal(id, duplicate)
try testing.expect_equal(id, same_id?) try testing.expect_equal(id, same_id?)
try testing.expect_equal(null, mutated_id) try testing.expect_equal(null, mutated_id)
try testing.expect_equal(null, invalid_str) try testing.expect_equal(null, invalid_str)
try testing.expect_equal("hello", hello_str?) try testing.expect_equal("hello", hello_str?)
} }
+51 -51
View File
@@ -1,74 +1,74 @@
import "@std/mem" import "@std/mem"
ArrayList proc($T type) type { ArrayList proc($T type) type {
return struct { return struct {
items []mut T items []mut T
capacity usize capacity usize
allocator mem.Allocator allocator mem.Allocator
} }
} }
init proc($T type, allocator mem.Allocator) ArrayList(T) { init proc($T type, allocator mem.Allocator) ArrayList(T) {
return ArrayList(T) { return ArrayList(T) {
items = mem.empty(T), items = mem.empty(T),
capacity = 0, capacity = 0,
allocator = allocator, allocator = allocator,
} }
} }
deinit proc($T type, list @mut ArrayList(T)) void { deinit proc($T type, list @mut ArrayList(T)) void {
allocation []mut T :: list.items.ptr[..list.capacity] allocation []mut T :: list.items.ptr[..list.capacity]
mem.free(list.allocator, allocation) mem.free(list.allocator, allocation)
list.items = mem.empty(T) list.items = mem.empty(T)
list.capacity = 0 list.capacity = 0
} }
reserve proc($T type, list @mut ArrayList(T), min_capacity usize) void ! mem.AllocError { hide reserve proc($T type, list @mut ArrayList(T), min_capacity usize) void ! mem.AllocError {
if min_capacity <= list.capacity { if min_capacity <= list.capacity {
return return
} }
new_capacity usize = 8 new_capacity usize = 8
if list.capacity >= 8 { if list.capacity >= 8 {
half usize :: divtrunc!(list.capacity, 2) half usize :: divtrunc!(list.capacity, 2)
if list.capacity > maxval!(usize) - half { if list.capacity > maxval!(usize) - half {
new_capacity = min_capacity new_capacity = min_capacity
} else { } else {
new_capacity = list.capacity + half new_capacity = list.capacity + half
} }
} }
if new_capacity < min_capacity { if new_capacity < min_capacity {
new_capacity = min_capacity new_capacity = min_capacity
} }
length usize :: list.items.len length usize :: list.items.len
allocation []mut T :: list.items.ptr[..list.capacity] allocation []mut T :: list.items.ptr[..list.capacity]
grown []mut T :: mem.realloc(list.allocator, allocation, new_capacity) catch |_| { grown []mut T :: mem.realloc(list.allocator, allocation, new_capacity) catch |_| {
return .out_of_memory return .out_of_memory
} }
list.items = grown.ptr[..length] list.items = grown.ptr[..length]
list.capacity = new_capacity list.capacity = new_capacity
return return
} }
append proc($T type, list @mut ArrayList(T), value T) void ! mem.AllocError { append proc($T type, list @mut ArrayList(T), value T) void ! mem.AllocError {
length usize :: list.items.len length usize :: list.items.len
if length == maxval!(usize) { if length == maxval!(usize) {
return .out_of_memory return .out_of_memory
} }
try reserve(list, length + 1) try reserve(list, length + 1)
list.items = list.items.ptr[..length + 1] list.items = list.items.ptr[..length + 1]
list.items[length] = value list.items[length] = value
return return
} }
pop proc($T type, list @mut ArrayList(T)) ?T { pop proc($T type, list @mut ArrayList(T)) ?T {
if (list.items.len == 0) return null if (list.items.len == 0) return null
value :: list.items[list.items.len - 1] value :: list.items[list.items.len - 1]
list.items = list.items.ptr[..list.items.len - 1] list.items = list.items.ptr[..list.items.len - 1]
return value return value
} }
clear proc($T type, list @mut ArrayList(T)) void { clear proc($T type, list @mut ArrayList(T)) void {
list.items = list.items.ptr[..0] list.items = list.items.ptr[..0]
} }
+15 -15
View File
@@ -2,31 +2,31 @@ import "@std/mem"
import "@std/testing" import "@std/testing"
handles_append test { handles_append test {
list ArrayList(i32) = init(mem.c_allocator) list ArrayList(i32) = init(mem.c_allocator)
defer deinit(&list) defer deinit(&list)
try append(&list, 42) try append(&list, 42)
try testing.expect_equal(1, list.items.len) try testing.expect_equal(1, list.items.len)
try testing.expect_equal(42, list.items[0]) try testing.expect_equal(42, list.items[0])
} }
handles_clear test { handles_clear test {
list ArrayList(i32) = init(mem.c_allocator) list ArrayList(i32) = init(mem.c_allocator)
defer deinit(&list) defer deinit(&list)
try append(&list, 42) try append(&list, 42)
clear(&list) clear(&list)
try testing.expect_equal(0, list.items.len) try testing.expect_equal(0, list.items.len)
} }
handles_reserve test { handles_reserve test {
list ArrayList(i32) = init(mem.c_allocator) list ArrayList(i32) = init(mem.c_allocator)
defer deinit(&list) defer deinit(&list)
try reserve(&list, 10) try reserve(&list, 10)
try testing.expect_equal(10, list.capacity) try testing.expect_equal(10, list.capacity)
try testing.expect_equal(0, list.items.len) try testing.expect_equal(0, list.items.len)
} }
+7 -7
View File
@@ -7,11 +7,11 @@
# Declarative and literal-only: one executable per build. List fields take an # Declarative and literal-only: one executable per build. List fields take an
# address-of an array literal (`&["raylib"]`) and default to empty. # address-of an array literal (`&["raylib"]`) and default to empty.
BuildConfig :: struct { BuildConfig :: struct {
name []u8 # output executable name under root/build name []u8 # output executable name under root/build
source []u8 # program package directory, relative to build.bro source []u8 # program package directory, relative to build.bro
libraries [][]u8 = &[] # library names to link (-l) libraries [][]u8 = &[] # library names to link (-l)
lib_paths [][]u8 = &[] # library search directories (-L) lib_paths [][]u8 = &[] # library search directories (-L)
includes [][]u8 = &[] # C include directories (-I) includes [][]u8 = &[] # C include directories (-I)
defines [][]u8 = &[] # C preprocessor defines (name or name=value) defines [][]u8 = &[] # C preprocessor defines (name or name=value)
links [][]u8 = &[] # extra linker inputs (object/source files, -framework pairs) links [][]u8 = &[] # extra linker inputs (object/source files, -framework pairs)
} }
+24 -20
View File
@@ -1,28 +1,32 @@
import "@ffi/c" import "@ffi/c"
import "@std/io" import "@std/io"
assert proc(ok bool) void {
if (!ok) unreachable
}
print proc($format []u8, $Args type, args Args) void { print proc($format []u8, $Args type, args Args) void {
writer io.Writer :: io.Writer{ writer io.Writer :: io.Writer{
context = null, context = null,
handle = io.Handle{ file_desc = c_int(io.Stream.stderr) }, handle = io.Handle{ file_desc = c_int(io.Stream.stderr) },
write = write, write = write,
} }
io.print(writer, format, Args, args) catch |_| {} io.print(writer, format, Args, args) catch |_| {}
} }
hide write proc(_ ?@mut anyopaque, handle io.Handle, bytes []u8) usize ! io.WriteError { hide write proc(_ ?@mut anyopaque, handle io.Handle, bytes []u8) usize ! io.WriteError {
request usize = bytes.len request usize = bytes.len
maximum usize :: usize(maxval!(c_long)) maximum usize :: usize(maxval!(c_long))
if request > maximum { if request > maximum {
request = maximum request = maximum
} }
while true { while true {
count c_long :: c.write(handle.file_desc, bytes.ptr, c_ulong(request)) count c_long :: c.write(handle.file_desc, bytes.ptr, c_ulong(request))
if count >= 0 { if count >= 0 {
return usize(count) return usize(count)
} }
if c.__error()?^ != c.EINTR { if c.__error()?^ != c.EINTR {
return .write_failed return .write_failed
} }
} }
} }
+45
View File
@@ -0,0 +1,45 @@
import "@std/meta"
EnumMap proc($E, $V type) type {
match typeinfo!(E) {
.enum |info|: return struct {
present [info.fields.len]mut bool # fixme: replace with bitset
values [info.fields.len]mut V
}
else: compile_error!("EnumMap key must be an enum")
}
}
init proc(
$E, $V type,
values meta.EnumFieldStruct(E, ?V, some!(null)),
) EnumMap(E, V) {
map EnumMap(E, V) = undefined
match typeinfo!(E) {
.enum |info|: expand for info.fields |field, i| {
map.present[i] = false
if field!(values, field.name) |value| {
map.present[i] = true
map.values[i] = value
}
}
else: compile_error!("EnumMap key must be an enum")
}
return map
}
get proc($E, $V type, map @EnumMap(E, V), key E) ?V {
# fixme: linear lookup; implement an enum index/discriminant map for O(1) lookup
match typeinfo!(E) {
.enum |info|: expand for info.fields |field, i| {
if key == field!(E, field.name) {
if (map.present[i]) return map.values[i]
return null
}
}
else: compile_error!("EnumMap key must be an enum")
}
}
+25
View File
@@ -0,0 +1,25 @@
import "@std/mem"
import "@std/testing"
TestEnum :: enum(u8) {
ident = 3
int = 8
eof = 21
}
handles_sparse_enum_get test {
names EnumMap(TestEnum, []u8) = init({
ident = "identifier",
int = "integer",
})
ident :: get(&names, TestEnum.ident)
try testing.expect_type(?[]u8, ident)
try testing.expect(mem.eql("identifier", ident?))
eof :: get(&names, TestEnum.eof)
try testing.expect_type(?[]u8, eof)
try testing.expect_equal(null, eof)
}
-45
View File
@@ -1,45 +0,0 @@
import "@std/meta"
EnumMap proc($E, $V type) type {
match typeinfo!(E) {
.enum |info|: return struct {
present [info.fields.len]mut bool # fixme: replace with bitset
values [info.fields.len]mut V
}
else: compile_error!("EnumMap key must be an enum")
}
}
init proc(
$E, $V type,
values meta.EnumFieldStruct(E, ?V, some!(null)),
) EnumMap(E, V) {
map EnumMap(E, V) = undefined
match typeinfo!(E) {
.enum |info|: expand for info.fields |field, i| {
map.present[i] = false
if field!(values, field.name) |value| {
map.present[i] = true
map.values[i] = value
}
}
else: compile_error!("EnumMap key must be an enum")
}
return map
}
get proc($E, $V type, map @EnumMap(E, V), key E) ?V {
# fixme: linear lookup; implement an enum index/discriminant map for O(1) lookup
match typeinfo!(E) {
.enum |info|: expand for info.fields |field, i| {
if key == field!(E, field.name) {
if (map.present[i]) return map.values[i]
return null
}
}
else: compile_error!("EnumMap key must be an enum")
}
}
-25
View File
@@ -1,25 +0,0 @@
import "@std/mem"
import "@std/testing"
TestEnum :: enum(u8) {
ident = 3
int = 8
eof = 21
}
handles_sparse_enum_get test {
names EnumMap(TestEnum, []u8) = init({
ident = "identifier",
int = "integer",
})
ident :: get(&names, TestEnum.ident)
try testing.expect_type(?[]u8, ident)
try testing.expect(mem.eql("identifier", ident?))
eof :: get(&names, TestEnum.eof)
try testing.expect_type(?[]u8, eof)
try testing.expect_equal(null, eof)
}
+101 -101
View File
@@ -3,154 +3,154 @@ import "@std/mem"
PutError :: enum { key_exists } PutError :: enum { key_exists }
Entry proc($K, $V type) type { Entry proc($K, $V type) type {
return struct { return struct {
# hash = 0 means empty # hash = 0 means empty
hash usize = 0 hash usize = 0
key K key K
value V value V
} }
} }
HashMap proc( HashMap proc(
$K, $V type, $K, $V type,
$hash_key proc(key K) usize, $hash_key proc(key K) usize,
$keys_eql proc(a, b K) bool, $keys_eql proc(a, b K) bool,
) type { ) type {
return struct { return struct {
entries []mut Entry(K, V) entries []mut Entry(K, V)
count usize count usize
allocator mem.Allocator allocator mem.Allocator
} }
} }
StringHashMap proc($V type) type { StringHashMap proc($V type) type {
return HashMap([]u8, V, str_hash, str_eql) return HashMap([]u8, V, str_hash, str_eql)
} }
init proc( init proc(
$K, $V type, $K, $V type,
$hash_key proc(key K) usize, $hash_key proc(key K) usize,
$keys_eql proc(a, b K) bool, $keys_eql proc(a, b K) bool,
allocator mem.Allocator, allocator mem.Allocator,
) HashMap(K, V, hash_key, keys_eql) { ) HashMap(K, V, hash_key, keys_eql) {
return HashMap(K, V, hash_key, keys_eql){ return HashMap(K, V, hash_key, keys_eql){
entries = mem.empty(Entry(K, V)), entries = mem.empty(Entry(K, V)),
count = 0, count = 0,
allocator = allocator, allocator = allocator,
} }
} }
#! free the entries in the hash map. #! free the entries in the hash map.
#! note: this operation invalidates the map. #! note: this operation invalidates the map.
deinit proc( deinit proc(
$K, $V type, $K, $V type,
$hash_key proc(key K) usize, $hash_key proc(key K) usize,
$keys_eql proc(a, b K) bool, $keys_eql proc(a, b K) bool,
map @HashMap(K, V, hash_key, keys_eql), map @HashMap(K, V, hash_key, keys_eql),
) void { mem.free(map.allocator, map.entries) } ) void { mem.free(map.allocator, map.entries) }
get proc( get proc(
$K, $V type, $K, $V type,
$hash_key proc(key K) usize, $hash_key proc(key K) usize,
$keys_eql proc(a, b K) bool, $keys_eql proc(a, b K) bool,
map @HashMap(K, V, hash_key, keys_eql), map @HashMap(K, V, hash_key, keys_eql),
key K, key K,
) ?V { ) ?V {
if (map.count == 0) return null if (map.count == 0) return null
hash :: normalize(hash_key(key)) hash :: normalize(hash_key(key))
idx usize = hash & (map.entries.len - 1) idx usize = hash & (map.entries.len - 1)
while true { while true {
entry :: map.entries[idx] entry :: map.entries[idx]
if (entry.hash == 0) return null if (entry.hash == 0) return null
if (entry.hash == hash and keys_eql(entry.key, key)) { if (entry.hash == hash and keys_eql(entry.key, key)) {
return entry.value return entry.value
} }
idx = (idx + 1) & (map.entries.len - 1) idx = (idx + 1) & (map.entries.len - 1)
} }
} }
put proc( put proc(
$K, $V type, $K, $V type,
$hash_key proc(key K) usize, $hash_key proc(key K) usize,
$keys_eql proc(a, b K) bool, $keys_eql proc(a, b K) bool,
map @mut HashMap(K, V, hash_key, keys_eql), map @mut HashMap(K, V, hash_key, keys_eql),
key K, key K,
value V, value V,
) void ! (PutError | mem.AllocError) { ) void ! (PutError | mem.AllocError) {
# check grow # check grow
threshold :: map.entries.len - divtrunc!(map.entries.len, 4) threshold :: map.entries.len - divtrunc!(map.entries.len, 4)
if (map.entries.len == 0 or map.count + 1 > threshold) { if (map.entries.len == 0 or map.count + 1 > threshold) {
old_entries :: map.entries old_entries :: map.entries
new_size :: if (old_entries.len > 0) old_entries.len * 2 else 8 new_size :: if (old_entries.len > 0) old_entries.len * 2 else 8
new_entries :: try mem.alloc(Entry(K, V), map.allocator, new_size) new_entries :: try mem.alloc(Entry(K, V), map.allocator, new_size)
# zero new entries # zero new entries
for (0..new_entries.len) |i| new_entries[i].hash = 0 for (0..new_entries.len) |i| new_entries[i].hash = 0
# move old entries # move old entries
for old_entries |entry| { for old_entries |entry| {
if (entry.hash == 0) continue if (entry.hash == 0) continue
# find an empty slot # find an empty slot
idx usize = entry.hash & (new_entries.len - 1) idx usize = entry.hash & (new_entries.len - 1)
while new_entries[idx].hash != 0 { while new_entries[idx].hash != 0 {
idx = (idx + 1) & (new_entries.len - 1) idx = (idx + 1) & (new_entries.len - 1)
} }
new_entries[idx] = entry new_entries[idx] = entry
} }
map.entries = new_entries map.entries = new_entries
mem.free(map.allocator, old_entries) mem.free(map.allocator, old_entries)
} }
# put new entry # put new entry
hash :: normalize(hash_key(key)) hash :: normalize(hash_key(key))
idx usize = hash & (map.entries.len - 1) idx usize = hash & (map.entries.len - 1)
while true { while true {
entry :: map.entries[idx] entry :: map.entries[idx]
if entry.hash == 0 { if entry.hash == 0 {
map.entries[idx] = Entry(K, V){ map.entries[idx] = Entry(K, V){
hash = hash, hash = hash,
key = key, key = key,
value = value, value = value,
} }
map.count += 1 map.count += 1
return return
} }
if entry.hash == hash and keys_eql(entry.key, key) { if entry.hash == hash and keys_eql(entry.key, key) {
return .key_exists return .key_exists
} }
idx = (idx + 1) & (map.entries.len - 1) idx = (idx + 1) & (map.entries.len - 1)
} }
} }
hide normalize proc(hash usize) usize { hide normalize proc(hash usize) usize {
# mapping both 0 and 1 to 1 is safe because equality resolves # mapping both 0 and 1 to 1 is safe because equality resolves
# collisions (since hash and key must both be equal). # collisions (since hash and key must both be equal).
if (hash == 0) return 1 if (hash == 0) return 1
return hash return hash
} }
#! FNV-1a hash implementation. #! FNV-1a hash implementation.
#! note: vulnerable to collision attacks. #! note: vulnerable to collision attacks.
hide str_hash proc(key []u8) usize { hide str_hash proc(key []u8) usize {
hash u32 = 2166136261 # offset basis hash u32 = 2166136261 # offset basis
prime u32 = 16777619 prime u32 = 16777619
for key |byte| { for key |byte| {
product u64 :: u64(hash xor u32(byte)) * prime product u64 :: u64(hash xor u32(byte)) * prime
hash = u32(product & u64(maxval!(u32))) hash = u32(product & u64(maxval!(u32)))
} }
return usize(hash) return usize(hash)
} }
hide str_eql proc(a, b []u8) bool { return mem.eql(a, b) } hide str_eql proc(a, b []u8) bool { return mem.eql(a, b) }
+6 -6
View File
@@ -2,12 +2,12 @@ import "@std/mem"
import "@std/testing" import "@std/testing"
handles_put_and_get test { handles_put_and_get test {
map StringHashMap(u32) = init(mem.c_allocator) map StringHashMap(u32) = init(mem.c_allocator)
defer deinit(&map) defer deinit(&map)
try put(&map, "key", 42) try put(&map, "key", 42)
value :: get(&map, "key") value :: get(&map, "key")
try testing.expect_type(?u32, value) try testing.expect_type(?u32, value)
try testing.expect_equal(42, value?) try testing.expect_equal(42, value?)
} }
+68 -68
View File
@@ -1,124 +1,124 @@
import "@ffi/c" import "@ffi/c"
File :: struct { File :: struct {
io Io io Io
handle Handle handle Handle
} }
FileMode :: enum { FileMode :: enum {
read_only read_only
write_only write_only
read_write read_write
} }
OpenError :: enum { OpenError :: enum {
open_failed open_failed
} }
CloseError :: enum { CloseError :: enum {
close_failed close_failed
} }
open proc(io Io, path [;0]u8, mode FileMode) File ! OpenError { open proc(io Io, path [;0]u8, mode FileMode) File ! OpenError {
handle Handle :: try io.vtable.open(io.context, path, mode) handle Handle :: try io.vtable.open(io.context, path, mode)
return File{ io = io, handle = handle } return File{ io = io, handle = handle }
} }
close proc(file File) void ! CloseError { close proc(file File) void ! CloseError {
try file.io.vtable.close(file.io.context, file.handle) try file.io.vtable.close(file.io.context, file.handle)
} }
reader proc(file File) Reader { reader proc(file File) Reader {
return Reader { return Reader {
context = file.io.context, context = file.io.context,
handle = file.handle, handle = file.handle,
read = file.io.vtable.read, read = file.io.vtable.read,
} }
} }
writer proc(file File) Writer { writer proc(file File) Writer {
return Writer { return Writer {
context = file.io.context, context = file.io.context,
handle = file.handle, handle = file.handle,
write = file.io.vtable.write, write = file.io.vtable.write,
} }
} }
hide system_read proc(_ ?@mut anyopaque, handle Handle, buffer []mut u8) usize ! ReadError { hide system_read proc(_ ?@mut anyopaque, handle Handle, buffer []mut u8) usize ! ReadError {
request usize = buffer.len request usize = buffer.len
maximum usize :: usize(maxval!(c_long)) maximum usize :: usize(maxval!(c_long))
if (request > maximum) request = maximum if (request > maximum) request = maximum
while true { while true {
count c_long :: c.read(handle.file_desc, buffer.ptr, c_ulong(request)) count c_long :: c.read(handle.file_desc, buffer.ptr, c_ulong(request))
if (count >= 0) return usize(count) if (count >= 0) return usize(count)
errno c_int :: c.__error()?^ errno c_int :: c.__error()?^
if (errno == c.EINTR) continue if (errno == c.EINTR) continue
if (errno == c.EBADF) return .not_open_for_reading if (errno == c.EBADF) return .not_open_for_reading
return .read_failed return .read_failed
} }
} }
hide system_write proc(_ ?@mut anyopaque, handle Handle, bytes []u8) usize ! WriteError { hide system_write proc(_ ?@mut anyopaque, handle Handle, bytes []u8) usize ! WriteError {
request usize = bytes.len request usize = bytes.len
maximum usize :: usize(maxval!(c_long)) maximum usize :: usize(maxval!(c_long))
if (request > maximum) request = maximum if (request > maximum) request = maximum
while true { while true {
count c_long :: c.write(handle.file_desc, bytes.ptr, c_ulong(request)) count c_long :: c.write(handle.file_desc, bytes.ptr, c_ulong(request))
if (count >= 0) return usize(count) if (count >= 0) return usize(count)
errno c_int :: c.__error()?^ errno c_int :: c.__error()?^
if (errno == c.EINTR) continue if (errno == c.EINTR) continue
if (errno == c.EBADF) return .not_open_for_writing if (errno == c.EBADF) return .not_open_for_writing
return .write_failed return .write_failed
} }
} }
hide system_open proc(_ ?@mut anyopaque, path [;0]u8, mode FileMode) Handle ! OpenError { hide system_open proc(_ ?@mut anyopaque, path [;0]u8, mode FileMode) Handle ! OpenError {
flags c_int = c.O_RDONLY flags c_int = c.O_RDONLY
match mode { match mode {
.read_only: flags = c.O_RDONLY .read_only: flags = c.O_RDONLY
.write_only: flags = c.O_WRONLY .write_only: flags = c.O_WRONLY
.read_write: flags = c.O_RDWR .read_write: flags = c.O_RDWR
} }
while true { while true {
fd c_int :: c.open(ptrcast!(c_char, path.ptr), flags) fd c_int :: c.open(ptrcast!(c_char, path.ptr), flags)
if (fd >= 0) return Handle{ file_desc = fd } if (fd >= 0) return Handle{ file_desc = fd }
if (c.__error()?^ != c.EINTR) return .open_failed if (c.__error()?^ != c.EINTR) return .open_failed
} }
} }
hide system_close proc(_ ?@mut anyopaque, handle Handle) void ! CloseError { hide system_close proc(_ ?@mut anyopaque, handle Handle) void ! CloseError {
if (c.close(handle.file_desc) != 0) return .close_failed if (c.close(handle.file_desc) != 0) return .close_failed
} }
hide system_stdin proc(_ ?@mut anyopaque) Handle { hide system_stdin proc(_ ?@mut anyopaque) Handle {
return Handle{ file_desc = c_int(Stream.stdin) } return Handle{ file_desc = c_int(Stream.stdin) }
} }
hide system_stdout proc(_ ?@mut anyopaque) Handle { hide system_stdout proc(_ ?@mut anyopaque) Handle {
return Handle{ file_desc = c_int(Stream.stdout) } return Handle{ file_desc = c_int(Stream.stdout) }
} }
hide system_stderr proc(_ ?@mut anyopaque) Handle { hide system_stderr proc(_ ?@mut anyopaque) Handle {
return Handle{ file_desc = c_int(Stream.stderr) } return Handle{ file_desc = c_int(Stream.stderr) }
} }
hide system_vtable IoVTable :: IoVTable { hide system_vtable IoVTable :: IoVTable {
read = system_read, read = system_read,
write = system_write, write = system_write,
open = system_open, open = system_open,
close = system_close, close = system_close,
stdin = system_stdin, stdin = system_stdin,
stdout = system_stdout, stdout = system_stdout,
stderr = system_stderr, stderr = system_stderr,
} }
hide system proc() Io { hide system proc() Io {
return Io { return Io {
context = null, context = null,
vtable = &system_vtable, vtable = &system_vtable,
} }
} }
+345 -305
View File
@@ -2,407 +2,447 @@ import "@ffi/c"
import "@std/meta" import "@std/meta"
ReadError :: enum { ReadError :: enum {
not_open_for_reading not_open_for_reading
read_failed read_failed
} }
WriteError :: enum { WriteError :: enum {
not_open_for_writing not_open_for_writing
write_failed write_failed
no_progress no_progress
} }
Handle :: union { Handle :: union {
file_desc c_int file_desc c_int
ptr @mut anyopaque ptr @mut anyopaque
} }
Stream :: enum(c_int) { Stream :: enum(c_int) {
stdin = c.STDIN_FILENO stdin = c.STDIN_FILENO
stdout = c.STDOUT_FILENO stdout = c.STDOUT_FILENO
stderr = c.STDERR_FILENO stderr = c.STDERR_FILENO
} }
Io :: struct { Io :: struct {
context ?@mut anyopaque context ?@mut anyopaque
vtable @IoVTable vtable @IoVTable
} }
IoVTable :: struct { IoVTable :: struct {
read @proc(context ?@mut anyopaque, handle Handle, buffer []mut u8) usize ! ReadError read @proc(context ?@mut anyopaque, handle Handle, buffer []mut u8) usize ! ReadError
write @proc(context ?@mut anyopaque, handle Handle, bytes []u8) usize ! WriteError write @proc(context ?@mut anyopaque, handle Handle, bytes []u8) usize ! WriteError
open @proc(context ?@mut anyopaque, path [;0]u8, mode FileMode) Handle ! OpenError open @proc(context ?@mut anyopaque, path [;0]u8, mode FileMode) Handle ! OpenError
close @proc(context ?@mut anyopaque, handle Handle) void ! CloseError close @proc(context ?@mut anyopaque, handle Handle) void ! CloseError
stdin @proc(context ?@mut anyopaque) Handle stdin @proc(context ?@mut anyopaque) Handle
stdout @proc(context ?@mut anyopaque) Handle stdout @proc(context ?@mut anyopaque) Handle
stderr @proc(context ?@mut anyopaque) Handle stderr @proc(context ?@mut anyopaque) Handle
} }
Reader :: struct { Reader :: struct {
context ?@mut anyopaque context ?@mut anyopaque
handle Handle handle Handle
read @proc(context ?@mut anyopaque, handle Handle, buffer []mut u8) usize ! ReadError read @proc(context ?@mut anyopaque, handle Handle, buffer []mut u8) usize ! ReadError
} }
Writer :: struct { Writer :: struct {
context ?@mut anyopaque context ?@mut anyopaque
handle Handle handle Handle
write @proc(context ?@mut anyopaque, handle Handle, bytes []u8) usize ! WriteError write @proc(context ?@mut anyopaque, handle Handle, bytes []u8) usize ! WriteError
} }
read proc(input Reader, buffer []mut u8) usize ! ReadError { read proc(input Reader, buffer []mut u8) usize ! ReadError {
if (buffer.len == 0) return 0 if (buffer.len == 0) return 0
count usize :: try input.read(input.context, input.handle, buffer) count usize :: try input.read(input.context, input.handle, buffer)
if (count > buffer.len) return .read_failed if (count > buffer.len) return .read_failed
return count return count
} }
write proc(output Writer, bytes []u8) usize ! WriteError { write proc(output Writer, bytes []u8) usize ! WriteError {
if (bytes.len == 0) return 0 if (bytes.len == 0) return 0
count usize :: try output.write(output.context, output.handle, bytes) count usize :: try output.write(output.context, output.handle, bytes)
if (count > bytes.len) return .write_failed if (count > bytes.len) return .write_failed
return count return count
} }
write_all proc(output Writer, bytes []u8) void ! WriteError { write_all proc(output Writer, bytes []u8) void ! WriteError {
offset usize = 0 offset usize = 0
while offset < bytes.len { while offset < bytes.len {
count usize :: try write(output, bytes[offset..]) count usize :: try write(output, bytes[offset..])
if (count == 0) return .no_progress if (count == 0) return .no_progress
offset += count offset += count
} }
} }
stdin proc(io Io) Reader { stdin proc(io Io) Reader {
return Reader { return Reader {
context = io.context, context = io.context,
handle = io.vtable.stdin(io.context), handle = io.vtable.stdin(io.context),
read = io.vtable.read, read = io.vtable.read,
} }
} }
stdout proc(io Io) Writer { stdout proc(io Io) Writer {
return Writer { return Writer {
context = io.context, context = io.context,
handle = io.vtable.stdout(io.context), handle = io.vtable.stdout(io.context),
write = io.vtable.write, write = io.vtable.write,
} }
} }
stderr proc(io Io) Writer { stderr proc(io Io) Writer {
return Writer { return Writer {
context = io.context, context = io.context,
handle = io.vtable.stderr(io.context), handle = io.vtable.stderr(io.context),
write = io.vtable.write, write = io.vtable.write,
} }
} }
print proc(output Writer, $format []u8, $Args type, args Args) void ! WriteError { print proc(output Writer, $format []u8, $Args type, args Args) void ! WriteError {
expand for parse_format(format.len, format, Args) |token| { expand for parse_format(format.len, format, Args) |token| {
match token.kind { match token.kind {
.unused: break .unused: break
.literal: try write_all(output, format[token.start..token.end]) .literal: try write_all(output, format[token.start..token.end])
.string: try write_all(output, field!(args, token.field)) .string: try write_all(output, field!(args, token.field))
.default: try write_default(output, field!(args, token.field)) .default: try write_default(output, field!(args, token.field))
.decimal: try write_decimal(output, field!(args, token.field)) .decimal: try write_decimal(output, field!(args, token.field))
.binary: try write_integer(output, field!(args, token.field), 2, false) .binary: try write_integer(output, field!(args, token.field), 2, false)
.octal: try write_integer(output, field!(args, token.field), 8, false) .octal: try write_integer(output, field!(args, token.field), 8, false)
.hex_lower: try write_integer(output, field!(args, token.field), 16, false) .hex_lower: try write_integer(output, field!(args, token.field), 16, false)
.hex_upper: try write_integer(output, field!(args, token.field), 16, true) .hex_upper: try write_integer(output, field!(args, token.field), 16, true)
.character: try write_character(output, field!(args, token.field)) .character: try write_character(output, field!(args, token.field))
else: try write_float(output, field!(args, token.field), true) else: try write_float(output, field!(args, token.field), true)
} }
} }
} }
hide write_integer_signed proc(output Writer, value i64, base u64, uppercase bool) void ! WriteError { hide write_integer_signed proc(output Writer, value i64, base u64, uppercase bool) void ! WriteError {
buffer [65]mut u8 = undefined buffer [65]mut u8 = undefined
end usize = buffer.len end usize = buffer.len
current i64 = value current i64 = value
while true { while true {
digit_value i64 :: rem!(current, i64(base)) digit_value i64 :: rem!(current, i64(base))
digit u8 = if (digit_value < 0) digit u8 = if (digit_value < 0)
u8(-digit_value) u8(-digit_value)
else else
u8(digit_value) u8(digit_value)
end -= 1 end -= 1
buffer[end] = if (digit < 10) buffer[end] = if (digit < 10)
'0' + digit '0' + digit
else if (uppercase) else if (uppercase)
'A' + digit - 10 'A' + digit - 10
else else
'a' + digit - 10 'a' + digit - 10
current = divtrunc!(current, i64(base)) current = divtrunc!(current, i64(base))
if (current == 0) break if (current == 0) break
}
if value < 0 {
end -= 1
buffer[end] = '-'
} }
try write_all(output, buffer[end..])
if value < 0 {
end -= 1
buffer[end] = '-'
}
try write_all(output, buffer[end..])
} }
hide write_integer_unsigned proc(output Writer, value u64, base u64, uppercase bool) void ! WriteError { hide write_integer_unsigned proc(output Writer, value u64, base u64, uppercase bool) void ! WriteError {
buffer [65]mut u8 = undefined buffer [65]mut u8 = undefined
end usize = buffer.len end usize = buffer.len
current u64 = value current u64 = value
while true { while true {
digit u8 :: u8(rem!(current, base)) digit u8 :: u8(rem!(current, base))
end -= 1 end -= 1
buffer[end] = if (digit < 10) buffer[end] = if (digit < 10)
'0' + digit '0' + digit
else if (uppercase) else if (uppercase)
'A' + digit - 10 'A' + digit - 10
else else
'a' + digit - 10 'a' + digit - 10
current = divtrunc!(current, base) current = divtrunc!(current, base)
if (current == 0) break if (current == 0) break
} }
try write_all(output, buffer[end..]) try write_all(output, buffer[end..])
} }
hide FormatTokenKind :: enum { hide FormatTokenKind :: enum {
unused unused
literal literal
default default
string string
decimal decimal
binary binary
octal octal
hex_lower hex_lower
hex_upper hex_upper
character character
scientific scientific
} }
hide FormatToken :: struct { hide FormatToken :: struct {
kind FormatTokenKind kind FormatTokenKind
start usize start usize
end usize end usize
field []u8 field []u8
} }
hide parse_format proc($N usize, $format []u8, $Args type) [N]mut FormatToken { hide parse_format proc($N usize, $format []u8, $Args type) [N]mut FormatToken {
tokens [N]mut FormatToken = undefined tokens [N]mut FormatToken = undefined
for (usize(0))..format.len |index| { for (usize(0))..format.len |index| {
tokens[index] = FormatToken{ kind = .unused, start = 0, end = 0, field = "" } tokens[index] = FormatToken{ kind = .unused, start = 0, end = 0, field = "" }
} }
field_count usize = 0 field_count usize = 0
match typeinfo!(Args) { match typeinfo!(Args) {
.record |r|: { .record |r|: {
if (!r.is_tuple) compile_error!("io.print arguments must be a tuple") if (!r.is_tuple) compile_error!("io.print arguments must be a tuple")
field_count = r.fields.len field_count = r.fields.len
} }
else: compile_error!("io.print arguments must be a tuple") else: compile_error!("io.print arguments must be a tuple")
} }
token_count usize = 0 token_count usize = 0
argument_count usize = 0 argument_count usize = 0
literal_start usize = 0 literal_start usize = 0
cursor usize = 0 cursor usize = 0
while cursor < format.len { while cursor < format.len {
byte :: format[cursor] byte :: format[cursor]
if byte == '{' { if byte == '{' {
if cursor + 1 >= format.len { if cursor + 1 >= format.len {
compile_error!("io.print format has an unmatched '{'") compile_error!("io.print format has an unmatched '{'")
} }
if cursor > literal_start { if cursor > literal_start {
tokens[token_count] = FormatToken{ tokens[token_count] = FormatToken{
kind = .literal, kind = .literal,
start = literal_start, start = literal_start,
end = cursor, end = cursor,
field = "", field = "",
} }
token_count += 1 token_count += 1
} }
next :: format[cursor + 1] next :: format[cursor + 1]
if next == '{' { if next == '{' {
tokens[token_count] = FormatToken{ tokens[token_count] = FormatToken{
kind = .literal, kind = .literal,
start = cursor, start = cursor,
end = cursor + 1, end = cursor + 1,
field = "", field = "",
} }
token_count += 1 token_count += 1
cursor += 2 cursor += 2
literal_start = cursor literal_start = cursor
continue continue
} }
kind FormatTokenKind = .default kind FormatTokenKind = .default
width usize = 2 width usize = 2
if next != '}' { if next != '}' {
if cursor + 2 >= format.len or format[cursor + 2] != '}' { if cursor + 2 >= format.len or format[cursor + 2] != '}' {
compile_error!("io.print format expects a one-character specifier") compile_error!("io.print format expects a one-character specifier")
} }
width = 3 width = 3
if (next == 's') kind = .string if (next == 's') kind = .string
else if (next == 'd') kind = .decimal else if (next == 'd') kind = .decimal
else if (next == 'b') kind = .binary else if (next == 'b') kind = .binary
else if (next == 'o') kind = .octal else if (next == 'o') kind = .octal
else if (next == 'x') kind = .hex_lower else if (next == 'x') kind = .hex_lower
else if (next == 'X') kind = .hex_upper else if (next == 'X') kind = .hex_upper
else if (next == 'c') kind = .character else if (next == 'c') kind = .character
else if (next == 'e') kind = .scientific else if (next == 'e') kind = .scientific
else compile_error!("io.print format has an unknown specifier") else compile_error!("io.print format has an unknown specifier")
} }
if argument_count >= field_count { if argument_count >= field_count {
compile_error!("io.print format argument count does not match the tuple") compile_error!("io.print format argument count does not match the tuple")
} }
tokens[token_count] = FormatToken{ tokens[token_count] = FormatToken{
kind = kind, kind = kind,
start = 0, start = 0,
end = 0, end = 0,
field = format_field_name(Args, argument_count), field = format_field_name(Args, argument_count),
} }
token_count += 1 token_count += 1
argument_count += 1 argument_count += 1
cursor += width cursor += width
literal_start = cursor literal_start = cursor
continue continue
} }
if byte == '}' { if byte == '}' {
if cursor + 1 >= format.len or format[cursor + 1] != '}' { if cursor + 1 >= format.len or format[cursor + 1] != '}' {
compile_error!("io.print format has an unmatched '}'") compile_error!("io.print format has an unmatched '}'")
} }
if cursor > literal_start { if cursor > literal_start {
tokens[token_count] = FormatToken{ tokens[token_count] = FormatToken{
kind = .literal, kind = .literal,
start = literal_start, start = literal_start,
end = cursor, end = cursor,
field = "", field = "",
} }
token_count += 1 token_count += 1
} }
tokens[token_count] = FormatToken{ tokens[token_count] = FormatToken{
kind = .literal, kind = .literal,
start = cursor, start = cursor,
end = cursor + 1, end = cursor + 1,
field = "", field = "",
} }
token_count += 1 token_count += 1
cursor += 2 cursor += 2
literal_start = cursor literal_start = cursor
continue continue
} }
cursor += 1 cursor += 1
} }
if literal_start < format.len { if literal_start < format.len {
tokens[token_count] = FormatToken{ tokens[token_count] = FormatToken{
kind = .literal, kind = .literal,
start = literal_start, start = literal_start,
end = format.len, end = format.len,
field = "", field = "",
} }
} }
if argument_count != field_count { if argument_count != field_count {
compile_error!("io.print format argument count does not match the tuple") compile_error!("io.print format argument count does not match the tuple")
} }
return tokens return tokens
} }
hide format_field_name proc($T type, index usize) []u8 { hide format_field_name proc($T type, index usize) []u8 {
match typeinfo!(T) { match typeinfo!(T) {
.record |r|: return r.fields[index].name .record |r|: return r.fields[index].name
else: compile_error!("io.print arguments must be a tuple") else: compile_error!("io.print arguments must be a tuple")
} }
}
hide distinct_value proc($Backing, $Distinct type, value Distinct) Backing {
return ptrcast!(Backing, &value)^
} }
hide scalar_or_distinct_type proc($T type) bool {
match typeinfo!(T) {
.bool: return true
.integer: return true
.float: return true
.distinct: return true
else: return false
}
}
hide write_integer proc(output Writer, $T type, value T, base u64, uppercase bool) void ! WriteError { hide write_integer proc(output Writer, $T type, value T, base u64, uppercase bool) void ! WriteError {
match typeinfo!(T) { match typeinfo!(T) {
.integer: if minval!(T) < 0 { .integer: if minval!(T) < 0 {
try write_integer_signed(output, i64(value), base, uppercase) try write_integer_signed(output, i64(value), base, uppercase)
} else { } else {
try write_integer_unsigned(output, u64(value), base, uppercase) try write_integer_unsigned(output, u64(value), base, uppercase)
} }
else: compile_error!("io.print integer format requires an integer argument") .distinct |backing|: if scalar_or_distinct_type(backing) {
} try write_integer(output, distinct_value(backing, T, value), base, uppercase)
} else {
compile_error!("io.print integer format requires an integer argument")
}
else: compile_error!("io.print integer format requires an integer argument")
}
} }
# note: libc keeps float formatting small; replace it with a native shortest-roundtrip writer if locale independence matters. # note: libc keeps float formatting small; replace it with a native shortest-roundtrip writer if locale independence matters.
hide write_float proc(output Writer, $T type, value T, scientific bool) void ! WriteError { hide write_float proc(output Writer, $T type, value T, scientific bool) void ! WriteError {
match typeinfo!(T) { match typeinfo!(T) {
.float: { .float: {
buffer [64]mut u8 = undefined buffer [64]mut u8 = undefined
count c_int = 0 count c_int = 0
if sizeof!(T) == 4 { if sizeof!(T) == 4 {
if (scientific) count = c.snprintf(ptrcast!(c_char, (&buffer).ptr), c_ulong(buffer.len), "%.8e", value) if (scientific) count = c.snprintf(ptrcast!(c_char, (&buffer).ptr), c_ulong(buffer.len), "%.8e", value)
else count = c.snprintf(ptrcast!(c_char, (&buffer).ptr), c_ulong(buffer.len), "%.9g", value) else count = c.snprintf(ptrcast!(c_char, (&buffer).ptr), c_ulong(buffer.len), "%.9g", value)
} else if scientific { } else if scientific {
count = c.snprintf(ptrcast!(c_char, (&buffer).ptr), c_ulong(buffer.len), "%.16e", value) count = c.snprintf(ptrcast!(c_char, (&buffer).ptr), c_ulong(buffer.len), "%.16e", value)
} else { } else {
count = c.snprintf(ptrcast!(c_char, (&buffer).ptr), c_ulong(buffer.len), "%.17g", value) count = c.snprintf(ptrcast!(c_char, (&buffer).ptr), c_ulong(buffer.len), "%.17g", value)
} }
if (count < 0 or usize(count) >= buffer.len) return .write_failed if (count < 0 or usize(count) >= buffer.len) return .write_failed
try write_all(output, buffer[0..usize(count)]) try write_all(output, buffer[0..usize(count)])
} }
else: compile_error!("io.print float format requires a float argument") .distinct |backing|: if scalar_or_distinct_type(backing) {
} try write_float(output, distinct_value(backing, T, value), scientific)
} else {
compile_error!("io.print float format requires a float argument")
}
else: compile_error!("io.print float format requires a float argument")
}
} }
hide write_decimal proc(output Writer, $T type, value T) void ! WriteError { hide write_decimal proc(output Writer, $T type, value T) void ! WriteError {
match typeinfo!(T) { match typeinfo!(T) {
.integer: try write_integer(output, value, 10, false) .integer: try write_integer(output, value, 10, false)
.float: try write_float(output, value, false) .float: try write_float(output, value, false)
else: compile_error!("io.print '{d}' requires an integer or float argument") .distinct |backing|: if scalar_or_distinct_type(backing) {
} try write_decimal(output, distinct_value(backing, T, value))
} else {
compile_error!("io.print '{d}' requires an integer or float argument")
}
else: compile_error!("io.print '{d}' requires an integer or float argument")
}
} }
hide write_character proc(output Writer, $T type, value T) void ! WriteError { hide write_character proc(output Writer, $T type, value T) void ! WriteError {
match typeinfo!(T) { match typeinfo!(T) {
.integer: { .integer: {
if minval!(T) < 0 or maxval!(T) > 255 { if minval!(T) < 0 or maxval!(T) > 255 {
compile_error!("io.print '{c}' requires an unsigned integer that fits in u8") compile_error!("io.print '{c}' requires an unsigned integer that fits in u8")
} }
buffer [1]u8 = [u8(value)] buffer [1]u8 = [u8(value)]
try write_all(output, buffer[..]) try write_all(output, buffer[..])
} }
else: compile_error!("io.print '{c}' requires an unsigned integer that fits in u8") .distinct |backing|: if scalar_or_distinct_type(backing) {
} try write_character(output, distinct_value(backing, T, value))
} else {
compile_error!("io.print '{c}' requires an unsigned integer that fits in u8")
}
else: compile_error!("io.print '{c}' requires an unsigned integer that fits in u8")
}
} }
hide write_default proc(output Writer, $T type, value T) void ! WriteError { hide write_default proc(output Writer, $T type, value T) void ! WriteError {
match typeinfo!(T) { match typeinfo!(T) {
.bool: if value { .bool: if value {
try write_all(output, "true") try write_all(output, "true")
} else { } else {
try write_all(output, "false") try write_all(output, "false")
} }
.integer: try write_integer(output, value, 10, false) .integer: try write_integer(output, value, 10, false)
.float: try write_float(output, value, false) .float: try write_float(output, value, false)
.array: try write_all(output, value) .array: try write_all(output, value)
.pointer: try write_all(output, value) .pointer: try write_all(output, value)
.slice: try write_all(output, value) .slice: try write_all(output, value)
.enum |enum_info|: { .enum |enum_info|: {
expand for enum_info.fields |field| { expand for enum_info.fields |field| {
if value == field!(T, field.name) { if value == field!(T, field.name) {
try write_all(output, ".") try write_all(output, ".")
try write_all(output, field.name) try write_all(output, field.name)
return return
} }
} }
return .write_failed return .write_failed
} }
else: compile_error!("io.print '{}' does not support this argument type") .distinct |backing|: if scalar_or_distinct_type(backing) {
} try write_default(output, distinct_value(backing, T, value))
} else {
compile_error!("io.print '{}' does not support this argument type")
}
else: compile_error!("io.print '{}' does not support this argument type")
}
} }
+107 -107
View File
@@ -1,119 +1,119 @@
import "@ffi/c" import "@ffi/c"
AllocError :: enum { AllocError :: enum {
out_of_memory out_of_memory
} }
Allocator :: struct { Allocator :: struct {
context ?@mut anyopaque context ?@mut anyopaque
vtable @AllocatorVTable vtable @AllocatorVTable
} }
AllocatorVTable :: struct { AllocatorVTable :: struct {
alloc @proc(context ?@mut anyopaque, size usize, alignment usize) ?*mut u8 alloc @proc(context ?@mut anyopaque, size usize, alignment usize) ?*mut u8
realloc @proc(context ?@mut anyopaque, memory ?*mut u8, old_size usize, new_size usize, alignment usize) ?*mut u8 realloc @proc(context ?@mut anyopaque, memory ?*mut u8, old_size usize, new_size usize, alignment usize) ?*mut u8
free @proc(context ?@mut anyopaque, memory ?*mut u8, size usize, alignment usize) void free @proc(context ?@mut anyopaque, memory ?*mut u8, size usize, alignment usize) void
} }
raw_alloc proc(allocator Allocator, size usize, alignment usize) ?*mut u8 { raw_alloc proc(allocator Allocator, size usize, alignment usize) ?*mut u8 {
return allocator.vtable.alloc(allocator.context, size, alignment) return allocator.vtable.alloc(allocator.context, size, alignment)
} }
raw_realloc proc(allocator Allocator, memory ?*mut u8, old_size usize, new_size usize, alignment usize) ?*mut u8 { raw_realloc proc(allocator Allocator, memory ?*mut u8, old_size usize, new_size usize, alignment usize) ?*mut u8 {
return allocator.vtable.realloc(allocator.context, memory, old_size, new_size, alignment) return allocator.vtable.realloc(allocator.context, memory, old_size, new_size, alignment)
} }
raw_free proc(allocator Allocator, memory ?*mut u8, size usize, alignment usize) void { raw_free proc(allocator Allocator, memory ?*mut u8, size usize, alignment usize) void {
allocator.vtable.free(allocator.context, memory, size, alignment) allocator.vtable.free(allocator.context, memory, size, alignment)
} }
eql proc($T type, left, right []T) bool { eql proc($T type, left, right []T) bool {
if (left.len != right.len) return false if (left.len != right.len) return false
for (0..left.len) |i| if (left[i] != right[i]) { for (0..left.len) |i| if (left[i] != right[i]) {
return false return false
} }
return true return true
} }
#! allocate memory for a slice of type `T` with `count` elements. #! allocate memory for a slice of type `T` with `count` elements.
alloc proc($T type, allocator Allocator, count usize) []mut T ! AllocError { alloc proc($T type, allocator Allocator, count usize) []mut T ! AllocError {
if (count == 0) return empty_slice(T, 0) if (count == 0) return empty_slice(T, 0)
element_size usize :: sizeof!(T) element_size usize :: sizeof!(T)
if (element_size == 0) return empty_slice(T, count) if (element_size == 0) return empty_slice(T, count)
if count > divtrunc!(maxval!(usize), element_size) { if count > divtrunc!(maxval!(usize), element_size) {
return .out_of_memory return .out_of_memory
} }
memory ?*mut u8 = raw_alloc(allocator, count * element_size, alignof!(T)) memory ?*mut u8 = raw_alloc(allocator, count * element_size, alignof!(T))
if memory |bytes| { if memory |bytes| {
pointer *mut T :: ptrcast!(T, bytes) pointer *mut T :: ptrcast!(T, bytes)
return pointer[..count] return pointer[..count]
} }
return .out_of_memory return .out_of_memory
} }
#! reallocate memory for a slice of type `T` with `new_count` elements. #! reallocate memory for a slice of type `T` with `new_count` elements.
#! reallocating with `new_count == 0` will free the memory and return an empty slice. #! reallocating with `new_count == 0` will free the memory and return an empty slice.
#! note: memory must be reallocated with the same allocator that was used to allocate it. #! note: memory must be reallocated with the same allocator that was used to allocate it.
realloc proc($T type, allocator Allocator, memory []mut T, new_count usize) []mut T ! AllocError { realloc proc($T type, allocator Allocator, memory []mut T, new_count usize) []mut T ! AllocError {
if new_count == memory.len { if new_count == memory.len {
return memory return memory
} }
if new_count == 0 { if new_count == 0 {
free(allocator, memory) free(allocator, memory)
return empty_slice(T, 0) return empty_slice(T, 0)
} }
element_size usize :: sizeof!(T) element_size usize :: sizeof!(T)
if (element_size == 0) return empty_slice(T, new_count) if (element_size == 0) return empty_slice(T, new_count)
if (new_count > divtrunc!(maxval!(usize), element_size)) return .out_of_memory if (new_count > divtrunc!(maxval!(usize), element_size)) return .out_of_memory
old_memory ?*mut u8 = null old_memory ?*mut u8 = null
old_size usize = 0 old_size usize = 0
if memory.len != 0 { if memory.len != 0 {
old_memory = ptrcast!(u8, memory.ptr) old_memory = ptrcast!(u8, memory.ptr)
old_size = memory.len * element_size old_size = memory.len * element_size
} }
resized ?*mut u8 = raw_realloc( resized ?*mut u8 = raw_realloc(
allocator, allocator,
old_memory, old_memory,
old_size, old_size,
new_count * element_size, new_count * element_size,
alignof!(T), alignof!(T),
) )
if resized |bytes| { if resized |bytes| {
pointer *mut T :: ptrcast!(T, bytes) pointer *mut T :: ptrcast!(T, bytes)
return pointer[..new_count] return pointer[..new_count]
} }
return .out_of_memory return .out_of_memory
} }
#! free memory allocated for a slice of type `T`. #! free memory allocated for a slice of type `T`.
#! note: memory must be freed with the same allocator that was used to allocate it. #! note: memory must be freed with the same allocator that was used to allocate it.
free proc($T type, allocator Allocator, memory []T) void { free proc($T type, allocator Allocator, memory []T) void {
if (memory.len == 0 or sizeof!(T) == 0) return if (memory.len == 0 or sizeof!(T) == 0) return
raw_free(allocator, ptrcast!( raw_free(allocator, ptrcast!(
u8, u8,
constcast!(memory).ptr), constcast!(memory).ptr),
memory.len * sizeof!(T), memory.len * sizeof!(T),
alignof!(T), alignof!(T),
) )
} }
#! get an empty slice of type `T` with `count` elements. #! get an empty slice of type `T` with `count` elements.
empty_slice proc($T type, count usize) []mut T { empty_slice proc($T type, count usize) []mut T {
pointer *mut T :: ptrcast!(T, (&empty_storage).ptr) pointer *mut T :: ptrcast!(T, (&empty_storage).ptr)
return pointer[..count] return pointer[..count]
} }
#! get an empty slice of type `T` with 0 elements. #! get an empty slice of type `T` with 0 elements.
empty proc($T type) []mut T { empty proc($T type) []mut T {
return empty_slice(T, 0) return empty_slice(T, 0)
} }
hide empty_storage [1]mut u64 = [0] hide empty_storage [1]mut u64 = [0]
@@ -121,70 +121,70 @@ hide empty_storage [1]mut u64 = [0]
hide malloc_alignment usize :: 16 # note: aarch64-macos libc malloc alignment assumption. hide malloc_alignment usize :: 16 # note: aarch64-macos libc malloc alignment assumption.
hide power_of_two proc(value usize) bool { hide power_of_two proc(value usize) bool {
if (value == 0) return false if (value == 0) return false
current usize = value current usize = value
while current > 1 { while current > 1 {
half usize = divtrunc!(current, 2) half usize = divtrunc!(current, 2)
if (half * 2 != current) return false if (half * 2 != current) return false
current = half current = half
} }
return true return true
} }
hide c_alloc proc(_ ?@mut anyopaque, size usize, alignment usize) ?*mut u8 { hide c_alloc proc(_ ?@mut anyopaque, size usize, alignment usize) ?*mut u8 {
if (power_of_two(alignment) == false) return null if (power_of_two(alignment) == false) return null
if (alignment <= malloc_alignment) return ptrcast!(u8, c.malloc(c_ulong(size))) if (alignment <= malloc_alignment) return ptrcast!(u8, c.malloc(c_ulong(size)))
memory [1]mut ?*mut anyopaque = [null] memory [1]mut ?*mut anyopaque = [null]
status c_int = c.posix_memalign((&memory).ptr, c_ulong(alignment), c_ulong(size)) status c_int = c.posix_memalign((&memory).ptr, c_ulong(alignment), c_ulong(size))
if (status != 0) return null if (status != 0) return null
return ptrcast!(u8, memory[0]) return ptrcast!(u8, memory[0])
} }
hide c_realloc proc( hide c_realloc proc(
_ ?@mut anyopaque, _ ?@mut anyopaque,
memory ?*mut u8, memory ?*mut u8,
old_size usize, old_size usize,
new_size usize, new_size usize,
alignment usize, alignment usize,
) ?*mut u8 { ) ?*mut u8 {
if (power_of_two(alignment) == false) return null if (power_of_two(alignment) == false) return null
if new_size == 0 { if new_size == 0 {
c.free(memory) c.free(memory)
return null return null
} }
if memory |old_memory| { if memory |old_memory| {
if alignment <= malloc_alignment { if alignment <= malloc_alignment {
return ptrcast!(u8, c.realloc(old_memory, c_ulong(new_size))) return ptrcast!(u8, c.realloc(old_memory, c_ulong(new_size)))
} }
new_memory ?*mut u8 = c_alloc(null, new_size, alignment) new_memory ?*mut u8 = c_alloc(null, new_size, alignment)
if new_memory |new_bytes| { if new_memory |new_bytes| {
copy_size usize = old_size copy_size usize = old_size
if (new_size < copy_size) copy_size = new_size if (new_size < copy_size) copy_size = new_size
memcopy!(new_bytes[..copy_size], old_memory[..copy_size]) memcopy!(new_bytes[..copy_size], old_memory[..copy_size])
c.free(old_memory) c.free(old_memory)
} }
return new_memory return new_memory
} }
return c_alloc(null, new_size, alignment) return c_alloc(null, new_size, alignment)
} }
hide c_free proc(_ ?@mut anyopaque, memory ?*mut u8, _ usize, _ usize) void { hide c_free proc(_ ?@mut anyopaque, memory ?*mut u8, _ usize, _ usize) void {
c.free(memory) c.free(memory)
} }
hide c_vtable AllocatorVTable :: AllocatorVTable { hide c_vtable AllocatorVTable :: AllocatorVTable {
alloc = c_alloc, alloc = c_alloc,
realloc = c_realloc, realloc = c_realloc,
free = c_free, free = c_free,
} }
c_allocator Allocator :: Allocator { c_allocator Allocator :: Allocator {
context = null, context = null,
vtable = &c_vtable, vtable = &c_vtable,
} }
+42 -42
View File
@@ -1,61 +1,61 @@
Layout :: enum { auto, c } Layout :: enum { auto, c }
ArrayInfo :: struct { ArrayInfo :: struct {
child type child type
len usize len usize
} }
FieldInfo :: struct { FieldInfo :: struct {
name []u8 name []u8
type type type type
index usize index usize
} }
RecordInfo :: struct { RecordInfo :: struct {
name ?[]u8 name ?[]u8
fields []FieldInfo fields []FieldInfo
is_tuple bool is_tuple bool
layout Layout layout Layout
} }
EnumInfo :: struct { EnumInfo :: struct {
fields []FieldInfo fields []FieldInfo
} }
TypeInfo :: union(enum) { TypeInfo :: union(enum) {
invalid void invalid void
void void void void
noreturn void noreturn void
anyopaque void anyopaque void
bool void bool void
integer void integer void
float void float void
array ArrayInfo array ArrayInfo
pointer void pointer void
slice void slice void
range void range void
optional void optional void
function void function void
enum EnumInfo enum EnumInfo
record RecordInfo record RecordInfo
union void union void
fallible void fallible void
distinct void distinct type
} }
EnumFieldStruct proc($E, $Field type, $default ?Field) type { EnumFieldStruct proc($E, $Field type, $default ?Field) type {
match typeinfo!(E) { match typeinfo!(E) {
.enum |info|: { .enum |info|: {
names [info.fields.len]mut []u8 = undefined names [info.fields.len]mut []u8 = undefined
field_types [info.fields.len]mut type = undefined field_types [info.fields.len]mut type = undefined
defaults [info.fields.len]mut ?Field = undefined defaults [info.fields.len]mut ?Field = undefined
expand for info.fields |field, index| { expand for info.fields |field, index| {
names[index] = field.name names[index] = field.name
field_types[index] = Field field_types[index] = Field
defaults[index] = default defaults[index] = default
} }
return struct_type!(.auto, names, field_types, defaults) return struct_type!(.auto, names, field_types, defaults)
} }
else: compile_error!("EnumFieldStruct key must be an enum") else: compile_error!("EnumFieldStruct key must be an enum")
} }
} }
+50 -33
View File
@@ -1,50 +1,67 @@
testing :: import "@std/testing" testing :: import "@std/testing"
TestTokenKind :: enum(u8) { TestTokenKind :: enum(u8) {
ident = 3 ident = 3
int = 8 int = 8
eof = 21 eof = 21
} }
TestNames :: alias EnumFieldStruct(TestTokenKind, ?[]u8, some!(null)) TestNames :: alias EnumFieldStruct(TestTokenKind, ?[]u8, some!(null))
TestArrayAlias :: alias [3]u16 TestArrayAlias :: alias [3]u16
TestInner :: distinct u16
TestOuter :: distinct TestInner
TestOuterAlias :: alias TestOuter
hide array_info_matches proc($Array, $Child type, $len usize) bool { hide array_info_matches proc($Array, $Child type, $len usize) bool {
match typeinfo!(Array) { match typeinfo!(Array) {
.array |info|: return info.child == Child and info.len == len .array |info|: return info.child == Child and info.len == len
else: return false else: return false
} }
} }
hide distinct_info_matches proc($Distinct, $Backing type) bool {
match typeinfo!(Distinct) {
.distinct |backing|: return backing == Backing
else: return false
}
}
array_reflection_exposes_child_and_logical_length test { array_reflection_exposes_child_and_logical_length test {
try testing.expect($(array_info_matches([4]i32, i32, 4))) try testing.expect($(array_info_matches([4]i32, i32, 4)))
try testing.expect($(array_info_matches([0]bool, bool, 0))) try testing.expect($(array_info_matches([0]bool, bool, 0)))
try testing.expect($(array_info_matches(TestArrayAlias, u16, 3))) try testing.expect($(array_info_matches(TestArrayAlias, u16, 3)))
try testing.expect($(array_info_matches([2]mut i64, i64, 2))) try testing.expect($(array_info_matches([2]mut i64, i64, 2)))
try testing.expect($(array_info_matches([2;0]u8, u8, 2))) try testing.expect($(array_info_matches([2;0]u8, u8, 2)))
} }
distinct_reflection_exposes_immediate_backing test {
try testing.expect($(distinct_info_matches(TestInner, u16)))
try testing.expect($(distinct_info_matches(TestOuter, TestInner)))
try testing.expect($(distinct_info_matches(TestOuterAlias, TestInner)))
}
enum_field_struct_defaults test { enum_field_struct_defaults test {
names TestNames = { names TestNames = {
ident = "identifier", ident = "identifier",
int = "integer", int = "integer",
} }
if field!(names, "ident") |value| { if field!(names, "ident") |value| {
try testing.expect(value.len == 10) try testing.expect(value.len == 10)
} else { } else {
try testing.expect(false) try testing.expect(false)
} }
if field!(names, "int") |value| { if field!(names, "int") |value| {
try testing.expect(value.len == 7) try testing.expect(value.len == 7)
} else { } else {
try testing.expect(false) try testing.expect(false)
} }
if field!(names, "eof") |_| { if field!(names, "eof") |_| {
try testing.expect(false) try testing.expect(false)
} }
empty TestNames = {} empty TestNames = {}
if field!(empty, "ident") |_| { if field!(empty, "ident") |_| {
try testing.expect(false) try testing.expect(false)
} }
} }
+1 -1
View File
@@ -1,5 +1,5 @@
import "@std/io" import "@std/io"
Init :: struct { Init :: struct {
io io.Io io io.Io
} }
+2 -2
View File
@@ -1,11 +1,11 @@
import "io" import "io"
import "enums" import "enums/enummap"
import "hashmap" import "hashmap"
import "arraylist" import "arraylist"
import "strmap" import "strmap"
Io :: alias io.Io Io :: alias io.Io
EnumMap :: alias enums.EnumMap EnumMap :: alias enummap.EnumMap
ArrayList :: alias arraylist.ArrayList ArrayList :: alias arraylist.ArrayList
StringHashMap :: alias hashmap.StringHashMap StringHashMap :: alias hashmap.StringHashMap
StringMap :: alias strmap.StringMap StringMap :: alias strmap.StringMap
+71 -71
View File
@@ -1,95 +1,95 @@
import "@std/mem" import "@std/mem"
StringMap proc($V type) type { StringMap proc($V type) type {
return struct { return struct {
keys [][]u8 keys [][]u8
values []V values []V
len_indexes []u32 len_indexes []u32
min_len u32 min_len u32
max_len u32 max_len u32
} }
} }
hide Pair proc($V type) type { hide Pair proc($V type) type {
return struct { []u8, V } return struct { []u8, V }
} }
#! initializes a static string map from a list of key-value pairs (constructed at compile-time). #! initializes a static string map from a list of key-value pairs (constructed at compile-time).
init proc($V type, $N usize, $entries [N]Pair(V)) StringMap(V) { init proc($V type, $N usize, $entries [N]Pair(V)) StringMap(V) {
if N > usize(maxval!(u32)) { if N > usize(maxval!(u32)) {
compile_error!("static string map has too many entries") compile_error!("static string map has too many entries")
} }
keys [N]mut []u8 = undefined keys [N]mut []u8 = undefined
values [N]mut V = undefined values [N]mut V = undefined
# assert no duplicate keys # assert no duplicate keys
for entries |entry, i| { for entries |entry, i| {
if entry.0.len > usize(maxval!(u32)) { if entry.0.len > usize(maxval!(u32)) {
compile_error!("static string map key is too long") compile_error!("static string map key is too long")
} }
for (0..i) |prior| if mem.eql(u8, entry.0, entries[prior].0) { for (0..i) |prior| if mem.eql(u8, entry.0, entries[prior].0) {
compile_error!("duplicate static string map key") compile_error!("duplicate static string map key")
} }
keys[i] = entry.0 keys[i] = entry.0
values[i] = entry.1 values[i] = entry.1
} }
if N == 0 { if N == 0 {
len_indexes [0]u32 = undefined len_indexes [0]u32 = undefined
return StringMap(V){ return StringMap(V){
keys = keys[..], keys = keys[..],
values = values[..], values = values[..],
len_indexes = len_indexes[..], len_indexes = len_indexes[..],
min_len = 0, min_len = 0,
max_len = 0, max_len = 0,
} }
} }
# fixme: insertion sort is compile-time O(N²); replace if large maps affect builds # fixme: insertion sort is compile-time O(N²); replace if large maps affect builds
for 1..N |i| { for 1..N |i| {
key :: keys[i] key :: keys[i]
value :: values[i] value :: values[i]
j usize = i j usize = i
while j > 0 and keys[j - 1].len > key.len : j -= 1 { while j > 0 and keys[j - 1].len > key.len : j -= 1 {
keys[j] = keys[j - 1] keys[j] = keys[j - 1]
values[j] = values[j - 1] values[j] = values[j - 1]
} }
keys[j] = key keys[j] = key
values[j] = value values[j] = value
} }
min_len u32 :: u32(keys[0].len) min_len u32 :: u32(keys[0].len)
max_len u32 :: u32(keys[N - 1].len) max_len u32 :: u32(keys[N - 1].len)
len_indexes [usize(max_len) + 1]mut u32 = undefined len_indexes [usize(max_len) + 1]mut u32 = undefined
entry_index usize = 0 entry_index usize = 0
for 0..=usize(max_len) |length| { for 0..=usize(max_len) |length| {
while entry_index < N and keys[entry_index].len < length : entry_index += 1 {} while entry_index < N and keys[entry_index].len < length : entry_index += 1 {}
len_indexes[length] = u32(entry_index) len_indexes[length] = u32(entry_index)
} }
return StringMap(V) { return StringMap(V) {
keys = keys[..], keys = keys[..],
values = values[..], values = values[..],
len_indexes = len_indexes[..], len_indexes = len_indexes[..],
min_len = min_len, min_len = min_len,
max_len = max_len, max_len = max_len,
} }
} }
get proc($V type, map @StringMap(V), key []u8) ?V { get proc($V type, map @StringMap(V), key []u8) ?V {
if (map.keys.len == 0 or key.len > maxval!(u32)) return null if (map.keys.len == 0 or key.len > maxval!(u32)) return null
length u32 = u32(key.len) length u32 = u32(key.len)
if (length < map.min_len or length > map.max_len) return null if (length < map.min_len or length > map.max_len) return null
idx usize = usize(map.len_indexes[usize(length)]) idx usize = usize(map.len_indexes[usize(length)])
while idx < map.keys.len : idx += 1 { while idx < map.keys.len : idx += 1 {
candidate :: map.keys[idx] candidate :: map.keys[idx]
if (candidate.len != key.len) return null # key not found if (candidate.len != key.len) return null # key not found
if mem.eql(u8, candidate, key) return map.values[idx] if mem.eql(u8, candidate, key) return map.values[idx]
} }
return null return null
} }
+1
View File
@@ -0,0 +1 @@
# todo
+71 -71
View File
@@ -1,85 +1,85 @@
import "@std/debug" import "@std/debug"
import "@std/mem" import "@std/mem"
Error :: enum { Error :: enum {
expectation_failed expectation_failed
} }
SourceLocation :: struct { SourceLocation :: struct {
file []u8 file []u8
line usize line usize
column usize column usize
} }
expect proc(condition bool, location SourceLocation) void ! Error { expect proc(condition bool, location SourceLocation) void ! Error {
if !condition { if !condition {
debug.print("{s}:{d}:{d}: expectation failed\n", { debug.print("{s}:{d}:{d}: expectation failed\n", {
location.file, location.file,
location.line, location.line,
location.column, location.column,
}) })
return .expectation_failed return .expectation_failed
} }
} }
expect_equal proc($T type, expected, actual T, location SourceLocation) void ! Error { expect_equal proc($T type, expected, actual T, location SourceLocation) void ! Error {
match typeinfo!(T) { match typeinfo!(T) {
.optional: { .optional: {
if expected |expected_value| { if expected |expected_value| {
if actual |actual_value| { if actual |actual_value| {
try expect_equal(expected_value, actual_value, location) try expect_equal(expected_value, actual_value, location)
return return
} }
debug.print("{s}:{d}:{d}: expected an optional value, found null\n", { debug.print("{s}:{d}:{d}: expected an optional value, found null\n", {
location.file, location.file,
location.line, location.line,
location.column, location.column,
}) })
return .expectation_failed return .expectation_failed
} }
if actual |_| { if actual |_| {
debug.print("{s}:{d}:{d}: expected null, found an optional value\n", { debug.print("{s}:{d}:{d}: expected null, found an optional value\n", {
location.file, location.file,
location.line, location.line,
location.column, location.column,
}) })
return .expectation_failed return .expectation_failed
} }
} }
.slice: if !mem.eql(expected, actual) { .slice: if !mem.eql(expected, actual) {
debug.print("{s}:{d}:{d}: expected and actual slices differ\n", { debug.print("{s}:{d}:{d}: expected and actual slices differ\n", {
location.file, location.file,
location.line, location.line,
location.column, location.column,
}) })
return .expectation_failed return .expectation_failed
} }
else: if expected != actual { else: if expected != actual {
debug.print("{s}:{d}:{d}: expected {}, found {}\n", { debug.print("{s}:{d}:{d}: expected {}, found {}\n", {
location.file, location.file,
location.line, location.line,
location.column, location.column,
expected, expected,
actual, actual,
}) })
return .expectation_failed return .expectation_failed
} }
} }
} }
expect_type proc($Expected, $Actual type, _ Actual, location SourceLocation) void ! Error { expect_type proc($Expected, $Actual type, _ Actual, location SourceLocation) void ! Error {
try expect($(Expected == Actual), location) try expect($(Expected == Actual), location)
} }
run proc(name []u8, callback *proc() void ! Error) bool { run proc(name []u8, callback *proc() void ! Error) bool {
callback() catch |_| { callback() catch |_| {
debug.print("{s}...[failed]\n", {name,}) debug.print("{s}...[failed]\n", {name,})
return false return false
} }
debug.print("{s}...[ok]\n", {name,}) debug.print("{s}...[ok]\n", {name,})
return true return true
} }
summary proc(passed, failed i32) void { summary proc(passed, failed i32) void {
debug.print("{d} passed, {d} failed\n", {passed, failed}) debug.print("{d} passed, {d} failed\n", {passed, failed})
} }