diff --git a/LANGUAGE.md b/LANGUAGE.md index 7d842aa..82706bc 100644 --- a/LANGUAGE.md +++ b/LANGUAGE.md @@ -59,6 +59,7 @@ roadmap and milestone history. - explicit comptime type parameters such as `max func($T type, a, b T) T`, specialized by type and omitted from the runtime ABI - forced typed comptime expressions such as `$sum(1, 2)`, `$Point { x = 1, y = 2 }`, and comptime value blocks such as `${ yield 4 }` - comptime execution for bodyful Brolang functions with mutable locals, loops, `defer`, `match`, `try`/`catch`, pointer/slice storage mutation, pointer captures, and calls through comptime-known function values +- comptime type factories such as `Box func($T type) type { return struct { value T } }`; calls like `Box(i32)` are concrete nominal types and may appear anywhere a type is expected - bodyful `c_func` definitions and bodyless `c_func` declarations with exact external symbol names - concrete-only C signatures, C variadic declarations/calls, and C default argument promotions - native function pointer values and types with `@func(...) R`, fallible `@func(...) R ! E`, optional `?@func(...) R`, and non-variadic native indirect calls @@ -74,7 +75,8 @@ roadmap and milestone history. ### standard packages -- `std/mem` allocator contract with a context pointer plus shared `AllocatorVTable`, raw byte operations `raw_alloc` / `raw_realloc` / `raw_free`, fallible typed `alloc(T, allocator, count)`, and typed `free(T, allocator, memory)`; failed nonzero raw reallocation preserves the original allocation, while zero size frees it +- `std/mem` allocator contract with raw byte operations, typed `empty` / `alloc` / `realloc` / `free`, overflow checks, zero-sized-type support, and failure-preserving reallocation +- `std/arraylist` generic `ArrayList(T)` with direct `items` slice access, explicit capacity, allocator ownership, fallible reserve/append, clear, and deinit ### compiler behavior @@ -91,7 +93,8 @@ roadmap and milestone history. - tuples and native Brolang variadic functions - exporting Brolang functions to C and broader target-specific C ABI lowering - non-plain C record layouts such as bitfields, packed records, flexible arrays, qualified fields, and C variadic record arguments -- typed heap allocation helpers, arenas, pools, build-mode heap policy, and escaping-allocation diagnostics +- arenas, pools, build-mode heap policy, and escaping-allocation diagnostics +- recursive type factories, type reflection, inferred type arguments, and type-producing unions/enums - broader Zig-style pointer/result casts beyond V1 `ptr_cast(T, ptr)` - sum-type ABI/layout polish, including dynamic tag-width shrinking, all-void channel collapse, and cross-module global-id determinism - backed/C enum composition and must-consume fallible linting diff --git a/README.md b/README.md index c86f193..3c9d90a 100644 --- a/README.md +++ b/README.md @@ -179,8 +179,10 @@ Current prototype features: - Demand-monomorphized Brolang and C-ABI functions - Integer and type comptime parameters (`func($N usize) [N]u8`, `func($T type, value T) T`) specialized by comptime argument - Forced typed comptime expressions (`$sum(1, 2)`, `$Point { x = 1, y = 2 }`) and comptime value blocks (`${ yield 4 }`) +- Zig-style comptime type factories returning anonymous native structs (`Box func($T type) type`, used as `Box(i32)`) - Comptime execution for bodyful Brolang functions with mutable locals, loops, `defer`, `match`, `try`/`catch`, pointer/slice storage mutation, pointer captures, and calls through comptime-known function values - Native function pointer values and types (`*func(...) R`, `*func(...) R ! E`, `?*func(...) R`) +- Typed allocation/reallocation through `std/mem` and generic dynamic arrays through `std/arraylist` - Bodyless concrete C function declarations with exact external symbol names - Bodyless manual and imported C variadic declarations with default argument promotions - Ordered linking of additional C sources, objects, archives, and libraries diff --git a/TODO.md b/TODO.md index 468f912..d4e9b29 100644 --- a/TODO.md +++ b/TODO.md @@ -743,9 +743,23 @@ - final open-constant defaults feed one last inference fixpoint before stale specializations are pruned -30. basic `std/arraylist` implementation using the new `std/mem` typed allocation +30. Zig-style type factories and basic `std/arraylist` (implemented; v1) + - comptime-only functions may return `type`; anonymous `struct { ... }` expressions and + factory calls such as `ArrayList(i32)` resolve to cached nominal concrete types + - factory parameters use the existing explicit `$T type` / integer comptime parameters; + normal comptime control flow and helper factory calls are supported + - type-factory calls work in signatures, nested types, struct literals, and type builtins; + runtime materialization and recursive specializations are diagnosed + - `std/mem` adds typed `empty` and failure-preserving `realloc`, including overflow, + zero-count, zero-sized-type, and alignment handling + - `std/arraylist.ArrayList(T)` exposes `items`, `capacity`, and `allocator`, with fallible + reserve/append, roughly 1.5x growth from 8, clear-without-free, and reusable deinit + - deferred: recursive factories, reflection, inferred type arguments, type-producing + unions/enums, pop/insert/remove/shrink/clone container operations -31. disallow arbitrary integer division +31. threading generic/polymorphic type information everywhere (init, deinit, etc.) might be annoying and verbose. consider whether generic structs could fit nicely to avoid this. + +32. disallow arbitrary integer division - take inspiration from zig - see also below for a word on unchecked casts - the user should be explicit about what they mean with integer division (e.g. `div`, `rem`, `trunc`) diff --git a/compiler/ast/ast.odin b/compiler/ast/ast.odin index 2dbb1c8..180fcf3 100644 --- a/compiler/ast/ast.odin +++ b/compiler/ast/ast.odin @@ -105,6 +105,7 @@ Expr_Kind :: enum u8 { Try, Catch, Function_Literal, + Anonymous_Struct_Type, } Expr :: struct { @@ -294,6 +295,7 @@ Module :: struct { unsupported: [dynamic]Unsupported, c_trampolines: [dynamic]Trampoline, strings: [dynamic]string, + type_fields: [dynamic]types.Field, type_store: types.Store, allocator: mem.Allocator, } @@ -312,6 +314,7 @@ init_module :: proc(allocator := context.allocator) -> Module { module.unsupported.allocator = allocator module.c_trampolines.allocator = allocator module.strings.allocator = allocator + module.type_fields.allocator = allocator return module } @@ -362,5 +365,6 @@ destroy_module :: proc(module: ^Module) { delete(module.unsupported) delete(module.c_trampolines) delete(module.strings) + delete(module.type_fields) types.destroy_store(&module.type_store) } diff --git a/compiler/checker/checker.odin b/compiler/checker/checker.odin index 0e0e79b..f6cfb4b 100644 --- a/compiler/checker/checker.odin +++ b/compiler/checker/checker.odin @@ -130,6 +130,19 @@ Import_Index_Entry :: struct { id: ast.Import_Id, } +Type_Factory_Entry :: struct { + template: ast.Function_Id, + values: []Comptime_Value, + result: types.Type, + resolving: bool, +} + +Generated_Type_Entry :: struct { + expr: ast.Expr_Id, + values: []Comptime_Value, + result: types.Type, +} + Checker :: struct { ast_module: ^ast.Module, diagnostics: ^source.Diagnostics, @@ -169,6 +182,8 @@ Checker :: struct { current_result: types.Type, current_build_ctx: ^Build_Ctx, current_comptime_values: []Comptime_Value, + type_factories: [dynamic]Type_Factory_Entry, + generated_types: [dynamic]Generated_Type_Entry, target: target.Target, allocator: mem.Allocator, } @@ -340,6 +355,8 @@ write_type_label :: proc(checker: ^Checker, builder: ^strings.Builder, value: ty write_type_label(checker, builder, item.child) strings.write_string(builder, " ! ") write_type_label(checker, builder, item.extra) + case .Type_Call: + strings.write_string(builder, "") case .Struct: strings.write_string(builder, "struct") case .Union: @@ -597,6 +614,8 @@ type_from_syntax :: proc( if params_changed || result != item.child { return types.function(store, resolved_params, result, item.c_abi, item.variadic) } + case .Type_Call: + return resolve_type_factory_call(checker, ast.Expr_Id(item.count_expr), pkg, file) } if changed { return types.intern(store, item) @@ -1130,10 +1149,133 @@ resolve_type_argument :: proc( value := types.find_named(&checker.module.types, u32(target_pkg), u32(expr.name), file=u32(expr_lookup_file(expr, file))) value = types.resolve_alias(value, &checker.module.types) return value, types.is_valid(value) + case .Call: + value := resolve_type_factory_call(checker, expr_id, pkg, file) + return value, types.is_valid(value) } return types.INVALID, false } +clone_comptime_values :: proc(values: []Comptime_Value, allocator: mem.Allocator) -> []Comptime_Value { + result := make([]Comptime_Value, len(values), allocator) + copy(result, values) + return result +} + +resolve_generated_struct_type :: proc(checker: ^Checker, expr_id: ast.Expr_Id, pkg: ast.Package_Id, file: ast.File_Id) -> types.Type { + for entry in checker.generated_types { + if entry.expr == expr_id && comptime_values_equal(entry.values, checker.current_comptime_values) { + return entry.result + } + } + if expr_id == ast.INVALID_EXPR || int(expr_id) >= len(checker.ast_module.exprs) { + return types.INVALID + } + expr := checker.ast_module.exprs[expr_id] + field_start := int(u32(expr.integer>>32)) + field_count := int(u32(expr.integer)) + if field_start < 0 || field_count < 0 || field_start+field_count > len(checker.ast_module.type_fields) { + return types.INVALID + } + template_fields := checker.ast_module.type_fields[field_start:field_start+field_count] + fields := make([]types.Field, len(template_fields), checker.allocator) + defer delete(fields, checker.allocator) + for field, index in template_fields { + resolved := type_from_syntax(checker, field.type, pkg, file) + if !is_runtime_type(checker, resolved) || types.is_void(resolved) { + source.addf(checker.diagnostics, expr.span, "anonymous struct field '%s' requires a concrete runtime type, got %s", symbol_text(checker, symbol.Id(field.name)), type_label(checker, resolved)) + return types.INVALID + } + fields[index] = types.Field{name=field.name, type=resolved} + } + result := types.struct_generated(&checker.module.types, fields) + append(&checker.generated_types, Generated_Type_Entry{ + expr=expr_id, + values=clone_comptime_values(checker.current_comptime_values, checker.allocator), + result=result, + }) + return result +} + +resolve_type_factory_call :: proc(checker: ^Checker, expr_id: ast.Expr_Id, pkg: ast.Package_Id, file: ast.File_Id) -> types.Type { + if expr_id == ast.INVALID_EXPR || int(expr_id) >= len(checker.ast_module.exprs) { + return types.INVALID + } + expr := checker.ast_module.exprs[expr_id] + if expr.kind != .Call || expr.left != ast.INVALID_EXPR { + source.add(checker.diagnostics, expr.span, "type position requires a direct type-factory call") + return types.INVALID + } + target_pkg, available := expr_package(checker, expr, pkg, file, true) + if !available { + return types.INVALID + } + template := find_template(checker, expr.name, target_pkg, expr_lookup_file(expr, file)) + if template == ast.INVALID_FUNCTION || int(template) >= len(checker.ast_module.functions) { + source.addf(checker.diagnostics, expr.span, "unknown type factory '%s'", symbol_text(checker, expr.name)) + return types.INVALID + } + function := checker.ast_module.functions[template] + if !is_type_metatype_syntax(checker, function.result) || types.is_valid(function.error) { + source.addf(checker.diagnostics, expr.span, "function '%s' does not return a type", symbol_text(checker, expr.name)) + return types.INVALID + } + for param in function.params { + if !param.comptime_value { + source.addf(checker.diagnostics, param.span, "type-factory parameter '%s' must be comptime", symbol_text(checker, param.name)) + return types.INVALID + } + } + if !valid_call_arity(function, len(expr.args)) { + source.addf(checker.diagnostics, expr.span, "type factory '%s' expects %d arguments, got %d", symbol_text(checker, expr.name), len(function.params), len(expr.args)) + return types.INVALID + } + values, ok := collect_comptime_values(checker, function, expr.args, pkg, file, true, checker.current_comptime_values) + defer delete(values, checker.allocator) + if !ok { + return types.INVALID + } + // A generic function's declaration is validated before it has a specialization. + // Leave calls containing its unresolved type parameters pending until then. + for value in values { + if value.kind != .Type { + continue + } + if item, item_ok := types.node(&checker.module.types, value.type); item_ok && item.kind == .Named && !item.declared { + return types.INVALID + } + } + for &entry in checker.type_factories { + if entry.template != template || !comptime_values_equal(entry.values, values) { + continue + } + if entry.resolving { + source.addf(checker.diagnostics, expr.span, "recursive type-factory specialization of '%s'", symbol_text(checker, expr.name)) + return types.INVALID + } + return entry.result + } + entry_index := len(checker.type_factories) + append(&checker.type_factories, Type_Factory_Entry{ + template=template, + values=clone_comptime_values(values, checker.allocator), + result=types.INVALID, + resolving=true, + }) + state := ct_state_make(checker, pkg, file) + value, flow, eval_ok := ct_eval_call_expr(&state, expr, function.result, 0) + result := types.INVALID + if eval_ok && flow.kind == .Normal && value != INVALID_CT_VALUE && int(value) < len(state.values) && state.values[value].kind == .Type { + result = types.Type(state.values[value].index) + } else if state.diagnostic == source.INVALID_DIAGNOSTIC { + source.addf(checker.diagnostics, expr.span, "type factory '%s' did not return a type", symbol_text(checker, expr.name)) + } + ct_state_destroy(&state) + checker.type_factories[entry_index].result = result + checker.type_factories[entry_index].resolving = false + return result +} + collect_comptime_values :: proc( checker: ^Checker, function: ast.Function, @@ -1362,7 +1504,7 @@ mark_expr_imports_used :: proc(checker: ^Checker, expr_id: ast.Expr_Id, file: as } case .Add, .Sub, .Mul, .Div, .Index, .Orelse, .Eq, .Ne, .Lt, .Le, .Gt, .Ge, .And, .Or, .Range: append(&stack, expr.left, expr.right) - case .Invalid, .Integer, .Float, .String, .Bool, .None, .Undefined, .Type, .Name: + case .Invalid, .Integer, .Float, .String, .Bool, .None, .Undefined, .Type, .Name, .Anonymous_Struct_Type: } } } @@ -2173,6 +2315,9 @@ infer_expr :: proc( case .Type: last = types.INVALID _ = pop(&stack) + case .Anonymous_Struct_Type: + last = types.INVALID + _ = pop(&stack) case .Integer: last = types.I64 if expr.integer <= 0x7fff_ffff_ffff_ffff { @@ -4789,7 +4934,10 @@ build_compound_expr :: proc( }) case .Struct_Literal: struct_type := types.INVALID - if symbol.is_valid(expr.name) { + if expr.left != ast.INVALID_EXPR { + struct_type, _ = resolve_type_argument(checker, expr.left, pkg, file) + struct_type = types.resolve_alias(struct_type, store) + } else if symbol.is_valid(expr.name) { target_pkg, available := expr_package(checker, expr, pkg, file, true) struct_type = types.find_named(store, u32(target_pkg), u32(expr.name), file=u32(expr_lookup_file(expr, file))) if available else types.INVALID struct_type = types.resolve_alias(struct_type, store) @@ -4977,7 +5125,7 @@ build_expr :: proc( template := ast.Function_Id(u32(expr.integer)) last = build_function_value(checker, template, expr.span, frame.expected) _ = pop(&stack) - case .Type: + case .Type, .Anonymous_Struct_Type: id := source.add(checker.diagnostics, expr.span, "type is not a runtime value") last = invalid_hir_expr(checker, expr.span, id) _ = pop(&stack) @@ -5294,6 +5442,12 @@ build_expr :: proc( continue } function := checker.ast_module.functions[template] + if is_type_metatype_syntax(checker, function.result) { + id := source.addf(checker.diagnostics, expr.span, "type factory '%s' is only valid in type position", symbol_text(checker, expr.name)) + last = invalid_hir_expr(checker, expr.span, id) + _ = pop(&stack) + continue + } if !valid_call_arity(function, len(expr.args)) { message := "function '%s' expects at least %d arguments, got %d" if function.variadic else "function '%s' expects %d arguments, got %d" @@ -8821,6 +8975,8 @@ check :: proc( checker.build_stack.allocator = allocator checker.cycle_stack.allocator = allocator checker.anon_globals.allocator = allocator + checker.type_factories.allocator = allocator + checker.generated_types.allocator = allocator build_symbol_indexes(&checker) checker.global_types = make([]types.Type, len(ast_module.globals), allocator) checker.global_demands = make([]types.Type, len(ast_module.globals), allocator) @@ -8864,6 +9020,14 @@ check :: proc( delete(checker.infer_stack) delete(checker.build_stack) delete(checker.cycle_stack) + for entry in checker.type_factories { + delete(entry.values, allocator) + } + for entry in checker.generated_types { + delete(entry.values, allocator) + } + delete(checker.type_factories) + delete(checker.generated_types) } for function, index in ast_module.functions { diff --git a/compiler/checker/comptime.odin b/compiler/checker/comptime.odin index 5ea2060..3b29fce 100644 --- a/compiler/checker/comptime.odin +++ b/compiler/checker/comptime.odin @@ -215,6 +215,7 @@ Ct_Value_Kind :: enum u8 { Pointer, Slice, Function, + Type, None, Optional_Some, Fallible, @@ -333,6 +334,9 @@ ct_state_make :: proc( if value.kind == .Integer { id := ct_add_value(&state, Ct_Value{kind=.Integer, type=value.type, integer=value.value}) ct_bind_value(&state, value.name, value.type, id, false) + } else if value.kind == .Type { + id := ct_add_value(&state, Ct_Value{kind=.Type, type=types.INVALID, index=u64(value.type)}) + ct_bind_value(&state, value.name, types.INVALID, id, false) } } return state @@ -532,6 +536,9 @@ ct_coerce_value :: proc(state: ^Ct_State, id: Ct_Value_Id, expected: types.Type, return id, true } value := state.values[id] + if value.kind == .Type && is_type_metatype_syntax(state.checker, expected) { + return id, true + } if types.equal(value.type, expected) { return id, true } @@ -914,7 +921,7 @@ ct_eval_expr :: proc( id := ct_add_value(state, Ct_Value{kind=.Integer, type=value.type, integer=value.value}) return id, ct_flow(.Normal), true } - return INVALID_CT_VALUE, ct_flow(.Normal), ct_failf(state, .Not_Comptime, expr.span, "type parameter '%s' is not a runtime value", symbol_text(checker, expr.name)) + return ct_add_value(state, Ct_Value{kind=.Type, type=types.INVALID, index=u64(value.type)}), ct_flow(.Normal), true } } else if find_import(checker, state.file, expr.qualifier) == ast.INVALID_IMPORT { if index, ok := ct_find_binding_index(state, expr.qualifier); ok { @@ -971,6 +978,12 @@ ct_eval_expr :: proc( return ct_eval_array_expr(state, expr, expected, depth+1) case .Struct_Literal: return ct_eval_struct_expr(state, expr, expected, depth+1) + case .Type: + resolved := type_from_syntax(checker, expr.type, state.pkg, state.file) + return ct_add_value(state, Ct_Value{kind=.Type, type=types.INVALID, index=u64(resolved)}), ct_flow(.Normal), types.is_valid(resolved) + case .Anonymous_Struct_Type: + resolved := resolve_generated_struct_type(checker, expr_id, state.pkg, state.file) + return ct_add_value(state, Ct_Value{kind=.Type, type=types.INVALID, index=u64(resolved)}), ct_flow(.Normal), types.is_valid(resolved) case .Enum_Literal: return ct_eval_enum_literal(state, expr, expected, depth+1) case .None: @@ -1150,7 +1163,7 @@ ct_eval_expr :: proc( return value, ct_flow(.Normal), true case .Slice: return ct_eval_slice_expr(state, expr, depth+1) - case .Type, .Undefined, .Keyed: + case .Undefined, .Keyed: return INVALID_CT_VALUE, ct_flow(.Normal), ct_fail(state, .Not_Comptime, expr.span, "expression cannot be evaluated at comptime") } return INVALID_CT_VALUE, ct_flow(.Normal), ct_fail(state, .Not_Comptime, expr.span, "expression cannot be evaluated at comptime") @@ -1216,7 +1229,10 @@ ct_eval_struct_expr :: proc(state: ^Ct_State, expr: ast.Expr, expected: types.Ty checker := state.checker store := &checker.module.types struct_type := types.INVALID - if symbol.is_valid(expr.name) { + if expr.left != ast.INVALID_EXPR { + struct_type, _ = resolve_type_argument(checker, expr.left, state.pkg, state.file) + struct_type = types.resolve_alias(struct_type, store) + } else if symbol.is_valid(expr.name) { target_pkg, available := expr_package(checker, expr, state.pkg, state.file, false) struct_type = types.find_named(store, u32(target_pkg), u32(expr.name), file=u32(expr_lookup_file(expr, state.file))) if available else types.INVALID struct_type = types.resolve_alias(struct_type, store) diff --git a/compiler/loader/loader.odin b/compiler/loader/loader.odin index 6bf375c..4374233 100644 --- a/compiler/loader/loader.odin +++ b/compiler/loader/loader.odin @@ -1402,6 +1402,9 @@ canonicalize_types :: proc(module: ^ast.Module, allocator: mem.Allocator) { for &statement in module.statements { statement.type = canonical_type(module, statement.type, mapping, visiting) } + for &field in module.type_fields { + field.type = canonical_type(module, field.type, mapping, visiting) + } for index := 0; index < original_count; index += 1 { _ = canonical_type(module, types.DYNAMIC_START+types.Type(index), mapping, visiting) } diff --git a/compiler/lower/lower.odin b/compiler/lower/lower.odin index 8f7ea4f..4fa15ce 100644 --- a/compiler/lower/lower.odin +++ b/compiler/lower/lower.odin @@ -396,11 +396,14 @@ lower_compound_expr :: proc(state: ^State, expr_id: hir.Expr_Id) -> ir.Instructi success_lbl := fresh_label(state) error_lbl := fresh_label(state) merge_lbl := fresh_label(state) - slot := append_instruction(state, ir.Instruction{ - op=.Alloca, span=expr.span, type=success, - target=ir.INVALID_REF, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION, - diagnostic=source.INVALID_DIAGNOSTIC, - }) + slot := ir.INVALID_INSTRUCTION + if !types.is_void(success) { + slot = append_instruction(state, ir.Instruction{ + op=.Alloca, span=expr.span, type=success, + target=ir.INVALID_REF, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION, + diagnostic=source.INVALID_DIAGNOSTIC, + }) + } append_instruction(state, ir.Instruction{ op=.Cond_Br, span=expr.span, type=types.VOID, integer=success_lbl, target=ir.Ref(u32(error_lbl)), a=ok, @@ -468,10 +471,12 @@ lower_compound_expr :: proc(state: ^State, expr_id: hir.Expr_Id) -> ir.Instructi } if expr.right != hir.INVALID_EXPR { fallback := lower_nested_expr(state, expr.right) - append_instruction(state, ir.Instruction{ - op=.Store, span=expr.span, type=success, - target=ir.INVALID_REF, a=slot, b=fallback, diagnostic=source.INVALID_DIAGNOSTIC, - }) + if !types.is_void(success) { + append_instruction(state, ir.Instruction{ + op=.Store, span=expr.span, type=success, + target=ir.INVALID_REF, a=slot, b=fallback, diagnostic=source.INVALID_DIAGNOSTIC, + }) + } append_instruction(state, ir.Instruction{ op=.Br, span=expr.span, type=types.VOID, integer=merge_lbl, target=ir.INVALID_REF, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION, @@ -505,11 +510,14 @@ lower_compound_expr :: proc(state: ^State, expr_id: hir.Expr_Id) -> ir.Instructi target=ir.INVALID_REF, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION, diagnostic=source.INVALID_DIAGNOSTIC, }) - append_instruction(state, ir.Instruction{ + merge := append_instruction(state, ir.Instruction{ op=.Label, span=expr.span, type=types.VOID, integer=merge_lbl, target=ir.INVALID_REF, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION, diagnostic=source.INVALID_DIAGNOSTIC, }) + if types.is_void(success) { + return merge + } return append_instruction(state, ir.Instruction{ op=.Load, span=expr.span, type=success, target=ir.INVALID_REF, a=slot, b=ir.INVALID_INSTRUCTION, diff --git a/compiler/parser/parser.odin b/compiler/parser/parser.odin index 2bc5d44..2c01f64 100644 --- a/compiler/parser/parser.odin +++ b/compiler/parser/parser.odin @@ -378,7 +378,7 @@ parse_type_atom :: proc(parser: ^Parser) -> ast.Type_Syntax { } name = advance(parser) } - return types.named( + named := types.named( &parser.module.type_store, u32(parser.pkg), u32(name.symbol), @@ -386,6 +386,14 @@ parse_type_atom :: proc(parser: ^Parser) -> ast.Type_Syntax { u32(parser.file), !symbol.is_valid(qualifier) && file_hidden_name(parser, name), ) + if current(parser).kind == .Left_Paren { + call := parse_call(parser, qualifier, first, name, 0) + return types.intern(&parser.module.type_store, types.Node{ + kind=.Type_Call, + count_expr=u32(call), + }) + } + return named } source.add(parser.diagnostics, tok.span, "expected a type") return types.INVALID @@ -589,6 +597,29 @@ parse_struct_literal :: proc( }) } +parse_anonymous_struct_type_expr :: proc(parser: ^Parser) -> ast.Expr_Id { + start := advance(parser) + fields: [dynamic]types.Field + fields.allocator = parser.module.allocator + if !parse_record_body(parser, &fields, "expected '{' after anonymous struct type") { + delete(fields) + return invalid_expr(parser, start.span, "invalid anonymous struct type") + } + end := previous(parser) + field_start := u32(len(parser.module.type_fields)) + field_count := u32(len(fields)) + append(&parser.module.type_fields, ..fields[:]) + delete(fields) + return add_expr(parser, ast.Expr{ + kind=.Anonymous_Struct_Type, + span=span_from(start.span, end.span), + integer=u64(field_start)<<32 | u64(field_count), + left=ast.INVALID_EXPR, + right=ast.INVALID_EXPR, + diagnostic=source.INVALID_DIAGNOSTIC, + }) +} + parse_integer_magnitude :: proc(text: string) -> (u64, bool) { value: u64 for byte in transmute([]byte)text { @@ -755,6 +786,8 @@ parse_primary :: proc(parser: ^Parser, nesting: int) -> ast.Expr_Id { }) case .Keyword_Func: return parse_function_literal(parser) + case .Keyword_Struct: + return parse_anonymous_struct_type_expr(parser) case .Left_Bracket: if starts_declared_type(parser) { start := tok @@ -842,7 +875,20 @@ parse_primary :: proc(parser: ^Parser, nesting: int) -> ast.Expr_Id { name = advance(parser) } if current(parser).kind == .Left_Paren { - return parse_call(parser, qualifier, first, name, nesting) + call := parse_call(parser, qualifier, first, name, nesting) + if current(parser).kind == .Left_Brace && !(parser.no_struct_literal && parser.delimiter_depth == 0) { + left_brace := advance(parser) + args, right_brace := parse_keyed_initializers(parser, left_brace, nesting, "expected '}' after struct literal") + return add_expr(parser, ast.Expr{ + kind=.Struct_Literal, + span=span_from(parser.module.exprs[call].span, right_brace.span), + args=args, + left=call, + right=ast.INVALID_EXPR, + diagnostic=source.INVALID_DIAGNOSTIC, + }) + } + return call } if current(parser).kind == .Left_Brace && !(parser.no_struct_literal && parser.delimiter_depth == 0) { return parse_struct_literal(parser, qualifier, first, name, nesting) diff --git a/compiler/types/types.odin b/compiler/types/types.odin index 773e189..fbf1cfc 100644 --- a/compiler/types/types.odin +++ b/compiler/types/types.odin @@ -75,6 +75,7 @@ Kind :: enum u8 { Struct, Union, Fallible, + Type_Call, } Node :: struct { @@ -359,6 +360,21 @@ struct_anonymous :: proc(store: ^Store, fields: []Field) -> Type { }) } +// Generated structs are nominal per comptime type-expression specialization. +// The checker owns canonicalization; this routine deliberately creates a fresh node. +struct_generated :: proc(store: ^Store, fields: []Field) -> Type { + start := u32(len(store.fields)) + append(&store.fields, ..fields) + id := DYNAMIC_START+Type(len(store.nodes)) + append(&store.nodes, Node{ + kind=.Struct, + field_start=start, + field_count=u32(len(fields)), + declared=true, + }) + return id +} + variant_id :: proc(store: ^Store, name: u32, payload: Type) -> (u16, bool) { for variant in store.variants { if variant.name == name && variant.payload == payload { diff --git a/compiler_tests.odin b/compiler_tests.odin index df3d6f7..252ddcf 100644 --- a/compiler_tests.odin +++ b/compiler_tests.odin @@ -6990,6 +6990,71 @@ comptime_type_params_compile_and_run :: proc(t: ^testing.T) { testing.expect_value(t, state.exit_code, 0) } +@(test) +type_factories_compile_and_run :: proc(t: ^testing.T) { + output := "/tmp/brolang-test-type-factory" + defer _ = os.remove(output) + status := compiler_core.compile_package("examples/programs/type_factory", output) + testing.expect_value(t, status, 0) + state := run_executable(output) + testing.expect_value(t, state.exit_code, 0) +} + +@(test) +arraylist_compiles_and_runs :: proc(t: ^testing.T) { + output := "/tmp/brolang-test-arraylist" + defer _ = os.remove(output) + status := compiler_core.compile_package("examples/programs/arraylist", output, nil, target.DEFAULT, cimport.Options{}, ".") + testing.expect_value(t, status, 0) + state := run_executable(output) + testing.expect_value(t, state.exit_code, 0) +} + +@(test) +type_factory_rejects_runtime_parameters_and_recursion :: proc(t: ^testing.T) { + texts := []string{ + `Bad func($T type, n usize) type { + return struct { value [n]T } +} +main func() void { value Bad(i32, 4) = undefined; _ = &value } +`, + `Loop func($T type) type { + return struct { next @Loop(T) } +} +main func() void { value Loop(i32) = undefined; _ = &value } +`, + `Box func($T type) type { + return struct { value T } +} +main func() void { _ = Box(i32) } +`, + `Bad func($T type) type { + return 1 +} +main func() void { value Bad(i32) = undefined; _ = &value } +`, + } + wanted := []string{"must be comptime", "recursive type-factory specialization", "only valid in type position", "cannot implicitly convert"} + for text, index in texts { + source_file := source.Source{path="test.bro", text=text} + diagnostics := source.init_diagnostics(&source_file) + symbols := symbol.init_table() + stream := lexer.lex(&source_file, &diagnostics, &symbols) + ast_module := parser.parse(&stream, &source_file, &diagnostics) + hir_module := checker.check(&ast_module, &diagnostics, &symbols) + found := false + for diagnostic in diagnostics.items { + found = found || strings.contains(diagnostic.message, wanted[index]) + } + testing.expect(t, found) + hir.destroy_module(&hir_module) + ast.destroy_module(&ast_module) + delete(stream.items) + symbol.destroy_table(&symbols) + source.destroy_diagnostics(&diagnostics) + } +} + @(test) comptime_eval_compile_and_run :: proc(t: ^testing.T) { output := "/tmp/brolang-test-comptime-eval" diff --git a/examples/programs/arraylist/main.bro b/examples/programs/arraylist/main.bro new file mode 100644 index 0000000..395808b --- /dev/null +++ b/examples/programs/arraylist/main.bro @@ -0,0 +1,77 @@ +arraylist :: import "@std/arraylist" +mem :: import "@std/mem" + +_fail_alloc func(_ ?*mut anyopaque, _ usize, _ usize) ?*mut u8 { + return none +} + +_fail_realloc func(_ ?*mut anyopaque, _ ?*mut u8, _ usize, _ usize, _ usize) ?*mut u8 { + return none +} + +_fail_free func(_ ?*mut anyopaque, _ ?*mut u8, _ usize, _ usize) void {} + +_fail_vtable mem.AllocatorVTable :: mem.AllocatorVTable { + alloc = _fail_alloc, + realloc = _fail_realloc, + free = _fail_free, +} + +_fail_allocator mem.Allocator :: mem.Allocator { + context = none, + vtable = &_fail_vtable, +} + +_noop func() void {} + +run func() i32 ! mem.AllocError { + values arraylist.ArrayList(i32) = arraylist.init(i32, mem.c_allocator) + defer arraylist.deinit(i32, &values) + if (values.items.len != 0 or values.capacity != 0) return 1 + + i usize = 0 + while i < 20 : i += 1 { + arraylist.append(i32, &values, i32(i)) catch |_| { + return .out_of_memory + } + } + if (values.items.len != 20 or values.capacity < 20) return 2 + if (values.items[0] != 0 or values.items[19] != 19) return 3 + values.items[3] = 33 + if (values.items[3] != 33) return 4 + + arraylist.reserve(i32, &values, 50) catch |_| { + return .out_of_memory + } + if (values.capacity < 50 or values.items.len != 20 or values.items[19] != 19) return 5 + capacity usize :: values.capacity + arraylist.clear(i32, &values) + if (values.items.len != 0 or values.capacity != capacity) return 6 + arraylist.append(i32, &values, 7) catch |_| { + return .out_of_memory + } + if (values.items.len != 1 or values.items[0] != 7 or values.capacity != capacity) return 7 + + empty_values arraylist.ArrayList([0]u8) = arraylist.init([0]u8, mem.c_allocator) + defer arraylist.deinit([0]u8, &empty_values) + zero [0]u8 :: [] + arraylist.append([0]u8, &empty_values, zero) catch |_| { + return .out_of_memory + } + if (empty_values.items.len != 1) return 8 + + failed arraylist.ArrayList(i32) = arraylist.init(i32, _fail_allocator) + failed_as_expected bool = false + arraylist.append(i32, &failed, 1) catch |_| { + failed_as_expected = true + yield _noop() + } + if (failed_as_expected == false or failed.items.len != 0 or failed.capacity != 0) return 9 + arraylist.deinit(i32, &failed) + + return 0 +} + +main func() i32 { + return run() catch 100 +} diff --git a/examples/programs/mem_allocator/typed_alloc.bro b/examples/programs/mem_allocator/typed_alloc.bro index 4dda088..cc9cbe4 100644 --- a/examples/programs/mem_allocator/typed_alloc.bro +++ b/examples/programs/mem_allocator/typed_alloc.bro @@ -92,6 +92,14 @@ typed_allocator_test func() i32 { typed[0] = 10 typed[3] = 20 if (typed[0] + typed[3] != 30) return 39 + typed = mem.realloc(i32, mem.c_allocator, typed, 8) catch |_| { + return 41 + } + if (typed.len != 8 or typed[0] != 10 or typed[3] != 20) return 42 + typed = mem.realloc(i32, mem.c_allocator, typed, 2) catch |_| { + return 43 + } + if (typed.len != 2 or typed[0] != 10) return 44 return 0 } diff --git a/examples/programs/type_factory/main.bro b/examples/programs/type_factory/main.bro new file mode 100644 index 0000000..c6ce39c --- /dev/null +++ b/examples/programs/type_factory/main.bro @@ -0,0 +1,44 @@ +Box func($T type) type { + return struct { + value T + } +} + +Buffer func($T type, $N usize) type { + if N == 0 { + return struct { + values [0]T + } + } + return struct { + values [N]T + } +} + +BoxAlias func($T type) type { + return Box(T) +} + +LocalAlias func($T type) type { + chosen :: T + return chosen +} + +make_box func($T type, value T) Box(T) { + return Box(T) { value = value } +} + +main func() i32 { + box Box(i32) :: make_box(i32, 42) + if (box.value != 42) return 1 + aliased BoxAlias(i32) :: box + if (aliased.value != 42) return 3 + local_alias LocalAlias(i32) :: 42 + if (local_alias != 42) return 6 + pointer @Box(i32) :: &box + if (pointer.value != 42) return 4 + buffer Buffer(u8, 4) :: Buffer(u8, 4) { values = [1, 2, 3, 4] } + if (buffer.values.len != 4) return 2 + if (size_of(Buffer(u8, 4)) != 4) return 5 + return 0 +} diff --git a/std/arraylist/arraylist.bro b/std/arraylist/arraylist.bro new file mode 100644 index 0000000..ccbd245 --- /dev/null +++ b/std/arraylist/arraylist.bro @@ -0,0 +1,67 @@ +mem :: import "@std/mem" + +ArrayList func($T type) type { + return struct { + items []mut T + capacity usize + allocator mem.Allocator + } +} + +init func($T type, allocator mem.Allocator) ArrayList(T) { + return ArrayList(T) { + items = mem.empty(T), + capacity = 0, + allocator = allocator, + } +} + +deinit func($T type, list @mut ArrayList(T)) void { + allocation []mut T :: list.items.ptr[..list.capacity] + mem.free(T, list.allocator, allocation) + list.items = mem.empty(T) + list.capacity = 0 +} + +reserve func($T type, list @mut ArrayList(T), minimum_capacity usize) void ! mem.AllocError { + if minimum_capacity <= list.capacity { + return _ + } + + new_capacity usize = 8 + if list.capacity >= 8 { + half usize :: list.capacity / 2 + if list.capacity > max_value(usize) - half { + new_capacity = minimum_capacity + } else { + new_capacity = list.capacity + half + } + } + if new_capacity < minimum_capacity { + new_capacity = minimum_capacity + } + + length usize :: list.items.len + allocation []mut T :: list.items.ptr[..list.capacity] + grown []mut T :: mem.realloc(T, list.allocator, allocation, new_capacity) catch |_| { + return .out_of_memory + } + list.items = grown.ptr[..length] + list.capacity = new_capacity + return _ +} + +append func($T type, list @mut ArrayList(T), value T) void ! mem.AllocError { + length usize :: list.items.len + if length == max_value(usize) { + return .out_of_memory + } + try reserve(T, list, length + 1) + list.items = list.items.ptr[..length + 1] + list.items[length] = value + return _ +} + +clear func($T type, list @mut ArrayList(T)) void { + list.items = list.items.ptr[..0] +} diff --git a/std/mem/mem.bro b/std/mem/mem.bro index 42c73ed..1d6c22f 100644 --- a/std/mem/mem.bro +++ b/std/mem/mem.bro @@ -34,6 +34,10 @@ _empty_slice func($T type, count usize) []mut T { return pointer[..count] } +empty func($T type) []mut T { + return _empty_slice(T, 0) +} + alloc func($T type, allocator Allocator, count usize) []mut T ! AllocError { if count == 0 { return _empty_slice(T, 0) @@ -55,6 +59,43 @@ alloc func($T type, allocator Allocator, count usize) []mut T ! AllocError { return .out_of_memory } +realloc func($T type, allocator Allocator, memory []mut T, new_count usize) []mut T ! AllocError { + if new_count == memory.len { + return memory + } + if new_count == 0 { + free(T, allocator, memory) + return _empty_slice(T, 0) + } + + element_size usize :: size_of(T) + if element_size == 0 { + return _empty_slice(T, new_count) + } + if new_count > max_value(usize) / element_size { + return .out_of_memory + } + + old_memory ?*mut u8 = none + old_size usize = 0 + if memory.len != 0 { + old_memory = ptr_cast(u8, memory.ptr) + old_size = memory.len * element_size + } + resized ?*mut u8 = raw_realloc( + allocator, + old_memory, + old_size, + new_count * element_size, + align_of(T), + ) + if resized |bytes| { + pointer *mut T :: ptr_cast(T, bytes) + return pointer[..new_count] + } + return .out_of_memory +} + free func($T type, allocator Allocator, memory []mut T) void { if memory.len != 0 and size_of(T) != 0 { raw_free(allocator, ptr_cast(u8, memory.ptr), memory.len * size_of(T), align_of(T))