diff --git a/LANGUAGE.md b/LANGUAGE.md index 3a53ec7..8924687 100644 --- a/LANGUAGE.md +++ b/LANGUAGE.md @@ -41,7 +41,7 @@ roadmap and milestone history. - source-order native structs, opaque nominal records with `Name :: opaque`, complete `c_struct { ... }`, keyed record literals, native untagged unions, and native tagged unions `union(Enum)` / `union(enum)` - named native struct fields may declare defaults with `field T = expression`; keyed literals use defaults for omitted fields and explicit initializers override them - void-payload tagged-union variants, anonymous struct payloads, contextual `.variant`, `.variant{payload}`, and `.variant{field = value}` construction -- native sum composition with `A | B` for unbacked enums and tagged unions, using program-global `u16` variant ids +- native sum composition with `A | B` for unbacked enums and tagged unions, optionally grouped as `(A | B)`, using program-global `u16` variant ids - fallible channel types `T ! E`, where `E` is a native enum/tagged union or supported sum composition; `void ! E` functions complete successfully on fallthrough, and void-success `catch` handlers may fall through without `yield` #### native record constraint fields @@ -98,7 +98,7 @@ fields. `_` is not a keyword member name. - `for` loops over ranges, arrays, slices, and pointers-to-arrays with copy captures, pointer captures `|@item|`, and optional `usize` index captures; `expand for` specializes a comptime aggregate into one checked body per element - `break`, `continue`, labeled `break :label`, labeled `continue :label`, and labeled plain blocks; `break :label` can cross nested scopes to exit a labeled block - bare block scopes, `defer`, and fallible-function `errdefer` with optional error capture; cleanup is block-scoped and LIFO -- bare void `return`, same-line `return value`, value blocks, value `if`, value loops, value `match`, and strictly value-producing `yield value` / `yield :label value` +- bare void `return`, same-line `return value`, value blocks, value `if` with implicit single-expression branches, value loops, value `match`, and strictly value-producing `yield value` / `yield :label value` - `match` statements/expressions over enums, tagged unions, and scalars, including exhaustiveness checks, payload captures, pointer payload captures, multi-pattern arms, and scalar range patterns - a final `expand |value|:` enum arm or `expand |payload[, tag]|:` tagged-union arm generates one specialized arm for each variant not covered earlier; enum values and optional tags are comptime-known, while union payloads keep their concrete variant type - fallible `try`, fallback `catch`, and `catch |e| { ... }` handler blocks @@ -188,7 +188,7 @@ as `math.divfloor(a, b)` resolve to ordinary functions. - comptime parameters may appear anywhere, are erased from the runtime ABI, and accept recursively stable booleans, integers, floats, types, immutable bytes, enums, fixed arrays, records/tuples, optionals, tagged unions, and bare function identities; equal structural values and aliases of one function declaration share specializations, while distinct declarations remain distinct and pointers, general slices, fallibles, ranges, untagged unions, and undefined values have no stable comptime identity - comptime parameters may be omitted when uniquely recoverable from runtime arguments, the immediate expected result, or exact type-factory provenance; `_` is an explicit inference hole - 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; `undefined` storage may be initialized at comptime, but remaining poison cannot be observed +- comptime execution for bodyful Brolang functions with mutable locals, loops, `defer`, `match`, `try`/`catch`, exact type `==`/`!=`, pointer/slice storage mutation, pointer captures, and calls through comptime-known function values; `undefined` storage may be initialized at comptime, but remaining poison cannot be observed - 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 - tuple types are unnamed-field structs (`struct { i32, []u8 }`), tuple values use `{1, "bro"}` / `{1,}` / `{}`, and fields use canonical numeric names such as `.0` - `typeinfo!`, `field!`, `compile_error!`, and semantic `expand for` provide compile-time record and enum reflection and heterogeneous static expansion without runtime metadata; enum reflection exposes declaration-ordered fields, reflected aggregates remain persistent compile-time values, and expand-loop `break` / `continue` must be selected entirely at comptime @@ -216,7 +216,7 @@ as `math.divfloor(a, b)` resolve to ordinary functions. - `std/io` explicit `Io` capabilities, provider-bound `Reader`/`Writer` handles, existing-file open/close operations, allocation-free `write_all`, and comptime-expanded writer-first `print`; formatting supports natural `{}`, byte `{s}`, decimal `{d}`, integer `{b}` / `{o}` / `{x}` / `{X}`, byte-character `{c}`, scientific float `{e}`, and `{{` / `}}`, with malformed formats and incompatible tuple fields rejected at comptime - entry points are either `main func() ...` or `main func(init process.Init) ...`; `std/process.Init` carries startup capabilities, currently only `io`, while the system provider remains hidden inside `std/io` - `std/debug.print` is an allocation-free, failure-ignoring stderr escape hatch independent of `process.Init` -- `std/testing` supplies fallible `expect` and expected-first `expect_equal`; direct calls through +- `std/testing` supplies fallible `expect`, expected-first `expect_equal`, and exact compile-time `expect_type`; direct calls through an alias of exactly `@std/testing` receive compiler-injected source locations ### compiler behavior diff --git a/compiler/checker/checker.odin b/compiler/checker/checker.odin index c0dcedb..071642e 100644 --- a/compiler/checker/checker.odin +++ b/compiler/checker/checker.odin @@ -164,6 +164,8 @@ Generated_Type_Entry :: struct { expr: ast.Expr_Id, values: []Comptime_Value, result: types.Type, + pkg: ast.Package_Id, + file: ast.File_Id, } Type_Factory_Origin :: struct { @@ -493,6 +495,10 @@ 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 .Sum: + 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: @@ -1136,7 +1142,14 @@ type_from_syntax :: proc( return types.INVALID } } - case .Pointer, .Slice, .Optional, .Range, .Distinct, .Enum, .Fallible: + return types.intern(store, item) + case .Pointer, .Slice, .Optional, .Range, .Fallible: + child := type_from_syntax(checker, item.child, pkg, file, depth+1) + extra := type_from_syntax(checker, item.extra, pkg, file, depth+1) + item.child = child + item.extra = extra + return types.intern(store, item) + case .Distinct, .Enum: child := type_from_syntax(checker, item.child, pkg, file, depth+1) extra := type_from_syntax(checker, item.extra, pkg, file, depth+1) changed = child != item.child || extra != item.extra @@ -1146,15 +1159,24 @@ type_from_syntax :: proc( params := types.params_for(store, value) resolved_params := make([]types.Type, len(params), checker.allocator) defer delete(resolved_params, checker.allocator) - params_changed := false for param, index in params { resolved_params[index] = type_from_syntax(checker, param.type, pkg, file, depth+1) - params_changed = params_changed || resolved_params[index] != param.type } result := type_from_syntax(checker, item.child, pkg, file, depth+1) - if params_changed || result != item.child { - return types.function(store, resolved_params, result, item.c_abi, item.variadic) + return types.function(store, resolved_params, result, item.c_abi, item.variadic) + case .Sum: + left := type_from_syntax(checker, item.child, pkg, file, depth+1) + right := type_from_syntax(checker, item.extra, pkg, file, depth+1) + composed, compose_error := types.compose_sum(store, left, right) + if compose_error == .Unsupported { + source.add(checker.diagnostics, source.Span{}, "only native unbacked enums and tagged unions can be composed with '|'") + return types.INVALID } + if compose_error == .Conflict { + source.add(checker.diagnostics, source.Span{}, "sum composition contains the same variant name with different payload types") + return types.INVALID + } + return composed case .Type_Call: return resolve_type_factory_call(checker, ast.Expr_Id(item.count_expr), pkg, file) } @@ -1176,6 +1198,31 @@ is_runtime_type :: proc(checker: ^Checker, value: types.Type) -> bool { return types.is_runtime_value(value, &checker.module.types) } +type_contains_unresolved_named :: proc(checker: ^Checker, value: types.Type, depth := 0) -> bool { + if depth > 64 { + return true + } + item, ok := types.node(&checker.module.types, value) + if !ok { + return false + } + if item.kind == .Named && !item.declared { + return true + } + if type_contains_unresolved_named(checker, item.child, depth+1) || + type_contains_unresolved_named(checker, item.extra, depth+1) { + return true + } + if item.kind == .Function { + for param in types.params_for(&checker.module.types, value) { + if type_contains_unresolved_named(checker, param.type, depth+1) { + return true + } + } + } + return false +} + is_comptime_value_type :: proc(checker: ^Checker, value: types.Type, depth := 0) -> bool { if depth > 256 || !types.is_valid(value) { return false @@ -1960,6 +2007,9 @@ explicit_comptime_argument_valid :: proc( if types.is_concrete_integer(type_from_syntax(checker, param.type, function.pkg, function.file)) { return eval_integer_constant_in_context(checker, arg, pkg, file).kind == .Value } + if type_pattern_mentions_comptime(checker, function, comptime_param_count(function), param.type) { + return true + } declared := type_from_syntax(checker, param.type, function.pkg, function.file) _, ok := eval_static_comptime_value(checker, param.name, arg, declared, pkg, file) return ok @@ -2324,11 +2374,14 @@ match_inferred_type_pattern :: proc( pattern_item, pattern_ok := types.node(store, pattern) if pattern_ok && pattern_item.qualifier == 0 && pattern_item.name != 0 { name := symbol.Id(pattern_item.name) - if _, is_binding := comptime_binding_index(function, prefix, name); is_binding { + if binding_index, is_binding := comptime_binding_index(function, prefix, name); is_binding { param, param_ok := comptime_param_for_name(function, name) if !param_ok || !is_comptime_type_param(checker, param) { return false } + if bound[binding_index] && can_implicitly_convert_type(checker, actual_type, values[binding_index].type) { + return true + } return bind_inferred_comptime( checker, function, prefix, values, bound, name, Comptime_Value{type=actual_type, kind=.Type}, span, diagnose, @@ -2584,7 +2637,6 @@ infer_call_comptime_values :: proc( values[binding_index] = Comptime_Value{name=param.name, type=declared, value=constant.value, kind=.Integer} bound[binding_index] = true } else { - declared := type_from_syntax(checker, param.type, function.pkg, function.file) available: [dynamic]Comptime_Value available.allocator = checker.allocator append(&available, ..checker.current_comptime_values) @@ -2593,6 +2645,10 @@ infer_call_comptime_values :: proc( append(&available, prior) } } + previous := checker.current_comptime_values + checker.current_comptime_values = available[:] + declared := type_from_syntax(checker, param.type, function.pkg, function.file) + checker.current_comptime_values = previous value, value_ok := eval_static_comptime_value( checker, param.name, arg_id, declared, pkg, file, available[:], diagnose, ) @@ -2880,6 +2936,8 @@ resolve_generated_struct_type :: proc(checker: ^Checker, expr_id: ast.Expr_Id, p expr=expr_id, values=clone_comptime_values(checker.current_comptime_values, checker.allocator), result=result, + pkg=pkg, + file=file, }) return result } @@ -2961,32 +3019,40 @@ resolve_type_factory_call :: proc(checker: ^Checker, expr_id: ast.Expr_Id, pkg: ct_state_destroy(&state) checker.type_factories[entry_index].result = result checker.type_factories[entry_index].resolving = false - if types.is_valid(result) { - generated := false - for entry in checker.generated_types { - if types.equal(entry.result, result) { - generated = true - break - } - } - if generated { - has_origin := false - for origin in checker.type_factory_origins { - if types.equal(origin.result, result) { - has_origin = true - break - } - } - if !has_origin { - append(&checker.type_factory_origins, Type_Factory_Origin{ - result=result, - template=template, - values=clone_comptime_values(values, checker.allocator), - }) - } + record_type_factory_origin(checker, result, template, values) + return result +} + +record_type_factory_origin :: proc( + checker: ^Checker, + result: types.Type, + template: ast.Function_Id, + values: []Comptime_Value, +) { + if !types.is_valid(result) { + return + } + generated := false + for entry in checker.generated_types { + if types.equal(entry.result, result) { + generated = true + break } } - return result + if !generated { + return + } + for origin in checker.type_factory_origins { + if origin.template == template && types.equal(origin.result, result) && + comptime_values_equal(origin.values, values) { + return + } + } + append(&checker.type_factory_origins, Type_Factory_Origin{ + result=result, + template=template, + values=clone_comptime_values(values, checker.allocator), + }) } collect_comptime_values :: proc( @@ -3056,7 +3122,15 @@ collect_comptime_values :: proc( }) continue } + resolution_values: [dynamic]Comptime_Value + resolution_values.allocator = checker.allocator + append(&resolution_values, ..extra_values) + append(&resolution_values, ..values[:]) + previous := checker.current_comptime_values + checker.current_comptime_values = resolution_values[:] declared := type_from_syntax(checker, param.type, function.pkg, function.file) + checker.current_comptime_values = previous + delete(resolution_values) if types.is_concrete_integer(declared) { constant := Constant{kind = .Not_Constant} if index < len(args) { @@ -4037,6 +4111,13 @@ validate_type_nodes :: proc(checker: ^Checker) { } } if item.kind == .Function { + unresolved := type_contains_unresolved_named(checker, item.child) + for param in types.params_for(&checker.module.types, id) { + unresolved = unresolved || type_contains_unresolved_named(checker, param.type) + } + if unresolved { + continue + } if item.c_abi { if types.kind(item.child, &checker.module.types) == .Fallible { source.add(checker.diagnostics, source.Span{}, "c_func pointer results cannot be fallible") @@ -4545,11 +4626,14 @@ infer_compound_expr :: proc( if initialized[index] { continue } - if field_default, ok := find_struct_field_default(checker, value, symbol.Id(field.name)); ok { + if field_default, default_values, ok := find_struct_field_default(checker, value, symbol.Id(field.name)); ok { + previous := checker.current_comptime_values + checker.current_comptime_values = default_values _ = infer_nested_expr( checker, field_default.expr, nil, field_default.pkg, field_default.file, demanded, expected=field.type, ) + checker.current_comptime_values = previous } } } @@ -6841,15 +6925,35 @@ find_struct_field_default :: proc( checker: ^Checker, struct_type: types.Type, name: symbol.Id, -) -> (ast.Struct_Field_Default, bool) { +) -> (ast.Struct_Field_Default, []Comptime_Value, bool) { resolved := types.resolve_alias(struct_type, &checker.module.types) for field_default in checker.ast_module.struct_field_defaults { if types.resolve_alias(field_default.record, &checker.module.types) == resolved && field_default.field == name { - return field_default, true + return field_default, nil, true } } - return {}, false + for entry in checker.generated_types { + if !types.equal(entry.result, resolved) || entry.expr == ast.INVALID_EXPR || + int(entry.expr) >= len(checker.ast_module.exprs) { + continue + } + expr := checker.ast_module.exprs[entry.expr] + fields := types.fields_for(&checker.module.types, resolved) + for field, index in fields { + if field.name != u32(name) || index >= len(expr.args) || expr.args[index] == ast.INVALID_EXPR { + continue + } + return ast.Struct_Field_Default{ + record=resolved, + field=name, + expr=expr.args[index], + pkg=entry.pkg, + file=entry.file, + }, entry.values, true + } + } + return {}, nil, false } find_tuple_field :: proc(checker: ^Checker, tuple_type: types.Type, index: u64) -> (int, types.Field, bool) { @@ -7656,10 +7760,6 @@ build_compound_expr :: proc( id := source.add(checker.diagnostics, expr.span, "'try' requires an enclosing fallible function") return invalid_hir_expr(checker, expr.span, id, success) } - if !types.equal(success, enclosing_success) { - id := source.add(checker.diagnostics, expr.span, "'try' success type must match the enclosing fallible result") - return invalid_hir_expr(checker, expr.span, id, success) - } error_type := types.fallible_error(channel_type, store) if !types.equal(error_type, enclosing_error) && !types.can_sum_widen(error_type, enclosing_error, store) { @@ -8076,11 +8176,14 @@ build_compound_expr :: proc( if values[index] != hir.INVALID_EXPR { continue } - if field_default, ok := find_struct_field_default(checker, struct_type, symbol.Id(field.name)); ok { + if field_default, default_values, ok := find_struct_field_default(checker, struct_type, symbol.Id(field.name)); ok { + previous := checker.current_comptime_values + checker.current_comptime_values = default_values values[index] = build_nested_expr( checker, field_default.expr, nil, global_reads, calls, field.type, field_default.pkg, field_default.file, ) + checker.current_comptime_values = previous values[index] = coerce_expr( checker, values[index], field.type, checker.ast_module.exprs[field_default.expr].span, @@ -11385,6 +11488,18 @@ emit_value_branch :: proc( ) -> bool { checker := ctx.checker n := len(branch_stmts) + if n == 1 && checker.ast_module.statements[branch_stmts[0]].kind == .Expression { + expr_stmt := checker.ast_module.statements[branch_stmts[0]] + expected := slot_type^ if slot^ != hir.INVALID_LOCAL else types.INVALID + value := build_expr(checker, expr_stmt.expr, ctx.locals^[:], ctx.global_reads, ctx.calls, expected, ctx.pkg, ctx.file) + if checker.module.exprs[value].kind == .Invalid { + ctx.problematic^ = true + return false + } + value = adopt_value_slot(ctx, slot, slot_type, value, checker.module.exprs[value].type, span) + emit_slot_assign(checker, out, slot^, value, span) + return true + } ends_in_yield := n > 0 && checker.ast_module.statements[branch_stmts[n - 1]].kind == .Yield && !symbol.is_valid(checker.ast_module.statements[branch_stmts[n - 1]].label) @@ -11992,9 +12107,7 @@ build_match_arm_body :: proc( return result[:], body_ok } -// build_value_arm appends a value-match arm's slot assignment(s) to `out`: a single bare -// expression yields implicitly; anything else reuses the value-branch rule (trailing -// `yield`, or exit on every path). +// build_value_arm appends a value-match arm's slot assignment(s) to `out`. build_value_arm :: proc( ctx: ^Build_Ctx, out: ^[dynamic]hir.Stmt_Id, @@ -12003,20 +12116,6 @@ build_value_arm :: proc( slot_type: ^types.Type, span: source.Span, ) -> bool { - checker := ctx.checker - if len(arm_body) == 1 && checker.ast_module.statements[arm_body[0]].kind == .Expression { - expr_stmt := checker.ast_module.statements[arm_body[0]] - expected := slot_type^ if slot^ != hir.INVALID_LOCAL else types.INVALID - value := build_expr(checker, expr_stmt.expr, ctx.locals^[:], ctx.global_reads, ctx.calls, expected, ctx.pkg, ctx.file) - if checker.module.exprs[value].kind == .Invalid { - ctx.problematic^ = true - return false - } - vtype := checker.module.exprs[value].type - value = adopt_value_slot(ctx, slot, slot_type, value, vtype, span) - emit_slot_assign(checker, out, slot^, value, span) - return true - } return emit_value_branch(ctx, out, arm_body, slot, slot_type, span) } diff --git a/compiler/checker/comptime.odin b/compiler/checker/comptime.odin index 22e96e4..db6ec8e 100644 --- a/compiler/checker/comptime.odin +++ b/compiler/checker/comptime.odin @@ -625,14 +625,19 @@ ct_coerce_value :: proc(state: ^Ct_State, id: Ct_Value_Id, expected: types.Type, } if value.kind == .Function { actual_item, actual_ok := types.node(store, value.type) - _, _, expected_function, expected_ok := types.function_pointer(expected, store) + _, _, expected_function, expected_pointer := types.function_pointer(expected, store) + expected_ok := expected_pointer + if expected_item, ok := types.node(store, expected); ok && expected_item.kind == .Function { + expected_function = expected + expected_ok = true + } if actual_ok && actual_item.kind == .Function && expected_ok && types.equal(value.type, expected_function) { value.type = expected return ct_add_value(state, value), true } _, _, actual_function, actual_pointer := types.function_pointer(value.type, store) - if actual_pointer && expected_ok && types.equal(actual_function, expected_function) { + if actual_pointer && expected_pointer && types.equal(actual_function, expected_function) { value.type = expected return ct_add_value(state, value), true } @@ -1522,14 +1527,17 @@ ct_eval_struct_expr :: proc(state: ^Ct_State, expr: ast.Expr, expected: types.Ty if values[index] != INVALID_CT_VALUE { continue } - field_default, has_default := find_struct_field_default(checker, struct_type, symbol.Id(field.name)) + field_default, default_values, has_default := find_struct_field_default(checker, struct_type, symbol.Id(field.name)) if !has_default { return INVALID_CT_VALUE, ct_flow(.Normal), ct_failf(state, .Not_Comptime, expr.span, "missing initializer for struct field '%s'", symbol_text(checker, symbol.Id(field.name))) } previous_pkg, previous_file := state.pkg, state.file + previous_comptime := checker.current_comptime_values state.pkg, state.file = field_default.pkg, field_default.file + checker.current_comptime_values = default_values value, flow, ok := ct_eval_expr(state, field_default.expr, field.type, depth+1) state.pkg, state.file = previous_pkg, previous_file + checker.current_comptime_values = previous_comptime if !ok || flow.kind != .Normal { return INVALID_CT_VALUE, flow, ok } @@ -2102,6 +2110,16 @@ ct_eval_binary :: proc(state: ^Ct_State, op: ast.Expr_Kind, left_id, right_id: C left := state.values[left_id] right := state.values[right_id] is_compare := op == .Eq || op == .Ne || op == .Lt || op == .Le || op == .Gt || op == .Ge + if left.kind == .Type || right.kind == .Type { + if left.kind != .Type || right.kind != .Type || (op != .Eq && op != .Ne) { + return INVALID_CT_VALUE, ct_flow(.Normal), ct_fail(state, .Not_Comptime, span, "type values only support '==' and '!=' with another type") + } + ok := types.equal(types.Type(left.index), types.Type(right.index)) + if op == .Ne { + ok = !ok + } + return ct_add_value(state, Ct_Value{kind=.Bool, type=types.BOOL, integer=1 if ok else 0}), ct_flow(.Normal), true + } if left.kind == .Bool && right.kind == .Bool { if op != .Eq && op != .Ne { return INVALID_CT_VALUE, ct_flow(.Normal), ct_fail(state, .Not_Comptime, span, "bool values only support '==' and '!='") @@ -2897,6 +2915,12 @@ ct_eval_template_call :: proc( if ct_value_references_dead_storage(state, result) { return INVALID_CT_VALUE, ct_flow(.Normal), ct_fail(state, .Not_Comptime, span, "comptime function returned a pointer to expired storage") } + if is_type_metatype_syntax(checker, function.result) && result != INVALID_CT_VALUE && + int(result) < len(state.values) && state.values[result].kind == .Type { + record_type_factory_origin( + checker, types.Type(state.values[result].index), template, comptime_values, + ) + } if types.is_valid(expected) { return ct_coerce_expr_value(state, result, expected, span) } diff --git a/compiler/loader/loader.odin b/compiler/loader/loader.odin index 7eaea74..27aa19e 100644 --- a/compiler/loader/loader.odin +++ b/compiler/loader/loader.odin @@ -1747,6 +1747,19 @@ canonical_type :: proc( mapping[index] = resolved return resolved } + if item.kind == .Sum { + left := canonical_type(module, item.child, mapping, visiting) + right := canonical_type(module, item.extra, mapping, visiting) + if resolved, compose_error := types.compose_sum(&module.type_store, left, right); compose_error == .None { + mapping[index] = resolved + return resolved + } + item.child = left + item.extra = right + resolved := types.intern(&module.type_store, item) + mapping[index] = resolved + return resolved + } if item.kind == .Function { params := make([]types.Type, int(item.field_count), context.temp_allocator) for param, param_index in types.params_for(&module.type_store, value) { diff --git a/compiler/parser/parser.odin b/compiler/parser/parser.odin index c1b5383..6567bf6 100644 --- a/compiler/parser/parser.odin +++ b/compiler/parser/parser.odin @@ -379,6 +379,13 @@ parse_type_atom :: proc(parser: ^Parser) -> ast.Type_Syntax { case .Keyword_Bool: advance(parser) return types.BOOL + case .Left_Paren: + advance(parser) + grouped := parse_type(parser) + if _, ok := allow(parser, .Right_Paren); !ok { + source.add(parser.diagnostics, current(parser).span, "expected ')' after grouped type") + } + return grouped case .Keyword_Func, .Keyword_C_Func: c_abi := tok.kind == .Keyword_C_Func advance(parser) @@ -447,6 +454,24 @@ parse_type_atom :: proc(parser: ^Parser) -> ast.Type_Syntax { return types.INVALID } +sum_syntax_requires_resolution :: proc(parser: ^Parser, value: types.Type, depth := 0) -> bool { + if depth > 64 { + return true + } + item, ok := types.node(&parser.module.type_store, value) + if !ok { + return false + } + if item.kind == .Named || item.kind == .Type_Call { + return true + } + if item.kind == .Alias || item.kind == .Sum { + return sum_syntax_requires_resolution(parser, item.child, depth+1) || + sum_syntax_requires_resolution(parser, item.extra, depth+1) + } + return false +} + parse_type_pipe_tail :: proc(parser: ^Parser, left: ast.Type_Syntax) -> ast.Type_Syntax { result := left for current(parser).kind == .Pipe { @@ -454,8 +479,12 @@ parse_type_pipe_tail :: proc(parser: ^Parser, left: ast.Type_Syntax) -> ast.Type right := parse_type_atom(parser) composed, compose_error := types.compose_sum(&parser.module.type_store, result, right) if compose_error == .Unsupported { - source.add(parser.diagnostics, operator.span, "only native unbacked enums and tagged unions can be composed with '|'") - result = types.INVALID + if sum_syntax_requires_resolution(parser, result) || sum_syntax_requires_resolution(parser, right) { + result = types.sum_syntax(&parser.module.type_store, result, right) + } else { + source.add(parser.diagnostics, operator.span, "only native unbacked enums and tagged unions can be composed with '|'") + result = types.INVALID + } } else if compose_error == .Conflict { source.add(parser.diagnostics, operator.span, "sum composition contains the same variant name with different payload types") result = types.INVALID @@ -738,9 +767,15 @@ parse_anonymous_struct_type_expr :: proc(parser: ^Parser) -> ast.Expr_Id { start := advance(parser) fields: [dynamic]types.Field fields.allocator = parser.module.allocator + defaults: [dynamic]ast.Expr_Id + defaults.allocator = parser.module.allocator tuple := false - if !parse_record_body(parser, &fields, "expected '{' after anonymous struct type", tuple_result=&tuple) { + if !parse_record_body( + parser, &fields, "expected '{' after anonymous struct type", + tuple_result=&tuple, defaults=&defaults, + ) { delete(fields) + delete(defaults) return invalid_expr(parser, start.span, "invalid anonymous struct type") } end := previous(parser) @@ -752,6 +787,7 @@ parse_anonymous_struct_type_expr :: proc(parser: ^Parser) -> ast.Expr_Id { kind=.Anonymous_Struct_Type, span=span_from(start.span, end.span), integer=u64(field_start)<<32 | u64(field_count), + args=defaults[:], tuple=tuple, left=ast.INVALID_EXPR, right=ast.INVALID_EXPR, diff --git a/compiler/testing.odin b/compiler/testing.odin index 9c76d15..6ffdbbd 100644 --- a/compiler/testing.odin +++ b/compiler/testing.odin @@ -122,11 +122,12 @@ inject_assertion_locations :: proc( ) { expect := symbol.intern(symbols, "expect") expect_equal := symbol.intern(symbols, "expect_equal") + expect_type := symbol.intern(symbols, "expect_type") original_count := len(module.exprs) for index in 0.. Type { return item.extra if ok && item.kind == .Fallible else INVALID } +sum_syntax :: proc(store: ^Store, left, right: Type) -> Type { + return intern(store, Node{kind=.Sum, child=left, extra=right}) +} + append_sum_variants :: proc(store: ^Store, value: Type, out: ^[dynamic]Sum_Variant) -> bool { item, ok := node(store, value) if !ok { diff --git a/compiler_tests.odin b/compiler_tests.odin index 78f98bf..76f8cad 100644 --- a/compiler_tests.odin +++ b/compiler_tests.odin @@ -5812,7 +5812,7 @@ main func() i32 { testing.expect(t, found_yield) testing.expect(t, found_misplaced_yield) - success_text := `A :: enum { + local_success_text := `A :: enum { a } B :: enum { @@ -5822,23 +5822,26 @@ Both :: alias A | B fs func() i64 ! A { return 1 } -bad_success func() i32 ! Both { +use_success func() void ! Both { x :: try fs() - return x + _ = x } main func() i32 { - return bad_success() catch 0 + use_success() catch |_| { return 1 } + return 0 } ` - directory := "/tmp/brolang-test-try-success-mismatch" - main_path := "/tmp/brolang-test-try-success-mismatch/main.bro" - output := "/tmp/brolang-test-try-success-mismatch-output" + directory := "/tmp/brolang-test-try-local-success" + main_path := "/tmp/brolang-test-try-local-success/main.bro" + output := "/tmp/brolang-test-try-local-success-output" _ = os2.remove_all(directory) defer _ = os2.remove_all(directory) defer _ = os.remove(output) testing.expect(t, os.make_directory(directory) == nil) - testing.expect(t, os.write_entire_file(main_path, transmute([]byte)success_text)) - testing.expect_value(t, compiler_core.compile_package(directory, output), 1) + testing.expect(t, os.write_entire_file(main_path, transmute([]byte)local_success_text)) + testing.expect_value(t, compiler_core.compile_package(directory, output), 0) + state := run_executable(output) + testing.expect_value(t, state.exit_code, 0) } @(test) @@ -6032,7 +6035,10 @@ OtherError :: union(enum) { } other void } -BothError :: alias PayloadError | OtherError +BothError :: alias (PayloadError | OtherError) +grouped_error func() void ! (PayloadError | OtherError) { + return .other +} accept func(value PayloadError) i32 { match value { .not_found |info|: return info.path + info.line @@ -13609,6 +13615,10 @@ milestone_44_struct_field_defaults_compile_and_run :: proc(t: ^testing.T) { Lists :: struct { items [][]u8 = &[] } +Generated func($T type, $initial i32) type { + return struct { value T = initial } +} + read func(value Config) i32 { return value.required + value.count } @@ -13619,10 +13629,12 @@ main func() i32 { defaults Config = Config{required = 35} overridden Config = Config{count = 1, enabled = false, name = "x", required = 40} lists Lists = Lists{} + generated Generated(i32, 9) = Generated(i32, 9){} if (ANSWER != 42) return 1 if (defaults.count != 7 or defaults.enabled == false) return 2 if (defaults.name.len != 3 or lists.items.len != 0) return 3 if (overridden.count != 1 or overridden.enabled or overridden.name.len != 1) return 4 + if (generated.value != 9) return 5 return 0 } ` @@ -13635,3 +13647,79 @@ main func() i32 { state := run_executable(output) testing.expect_value(t, state.exit_code, 0) } + +@(test) +dependent_comptime_callbacks_and_imported_grouped_sums_compile :: proc(t: ^testing.T) { + directory := "/tmp/brolang-test-dependent-callbacks" + app := "/tmp/brolang-test-dependent-callbacks/app" + dependency := "/tmp/brolang-test-dependent-callbacks/dependency" + app_path := "/tmp/brolang-test-dependent-callbacks/app/main.bro" + dependency_path := "/tmp/brolang-test-dependent-callbacks/dependency/errors.bro" + output := "/tmp/brolang-test-dependent-callbacks-output" + _ = os2.remove_all(directory) + defer _ = os2.remove_all(directory) + defer _ = os.remove(output) + testing.expect(t, os2.make_directory_all(app) == nil) + testing.expect(t, os2.make_directory_all(dependency) == nil) + + dependency_text := `AllocError :: enum { out_of_memory } +` + app_text := `dependency :: import "../dependency" + +PutError :: enum { key_exists } + +Entry func($K, $V type) type { + return struct { key K, value V } +} + +Map func( + $K, $V type, + $hash_key func(key K) usize, + $keys_eql func(a, b K) bool, +) type { + return struct { entry Entry(K, V) } +} + +hash func(key []u8) usize { return key.len } +eql func(a, b []u8) bool { return a.len == b.len } + +StringMap func($V type) type { + return Map([]u8, V, hash, eql) +} + +make func( + $K, $V type, + $hash_key func(key K) usize, + $keys_eql func(a, b K) bool, +) Map(K, V, hash_key, keys_eql) { + return Map(K, V, hash_key, keys_eql) { + entry = Entry(K, V) {key = "", value = 0}, + } +} + +put func( + $K, $V type, + $hash_key func(key K) usize, + $keys_eql func(a, b K) bool, + map @Map(K, V, hash_key, keys_eql), + key K, +) void { + _ = map + _ = key +} + +fallible func() void ! (PutError | dependency.AllocError) { return .key_exists } + +main func() void { + map StringMap(u32) = make() + put(&map, "key") + _ = map + fallible() catch |_| {} +} +` + testing.expect(t, os.write_entire_file(dependency_path, transmute([]byte)dependency_text)) + testing.expect(t, os.write_entire_file(app_path, transmute([]byte)app_text)) + testing.expect_value(t, compiler_core.compile_package(app, output), 0) + state := run_executable(output) + testing.expect_value(t, state.exit_code, 0) +} diff --git a/examples/programs/comptime_type_params/main.bro b/examples/programs/comptime_type_params/main.bro index 7cb431a..7567e16 100644 --- a/examples/programs/comptime_type_params/main.bro +++ b/examples/programs/comptime_type_params/main.bro @@ -38,6 +38,10 @@ fixed_len func($T type, $N usize, value @Fixed(T, N)) usize { return value.values.len } +same_type func($Expected, $Actual type, _ Actual) bool { + return $(Expected == Actual) +} + take_i32 func(value i32) i32 { return value } @@ -87,5 +91,12 @@ main func() i32 { _ = assigned _ = take_i32(zero()) _ = return_zero() + optional ?u32 = none + if !same_type(?u32, optional) { + return 8 + } + if same_type(?u16, optional) { + return 9 + } return 0 } diff --git a/examples/programs/yield/main.bro b/examples/programs/yield/main.bro index 047ffd1..2ea2df2 100644 --- a/examples/programs/yield/main.bro +++ b/examples/programs/yield/main.bro @@ -85,6 +85,11 @@ vif_defer func() i32 { return r # 5 } +vif_expression func(old_entries []u8) usize { + new_size :: if (old_entries.len > 0) old_entries.len * 2 else 8 + return new_size +} + # --- value loops (milestone 20.5) -------------------------------------------- # Labeled `for` used as a value: `yield :blk i` exits early with a value, the @@ -306,6 +311,8 @@ main func() i32 { if (vif_reassign(0) != 7) return 110 if (vif_reassign(9) != 9) return 111 if (vif_defer() != 5) return 112 + if (vif_expression("") != 8) return 136 + if (vif_expression("abc") != 6) return 137 if (loop_search() != 0) return 113 if (loop_none() != 0) return 114 diff --git a/std/arraylist/arraylist.test.bro b/std/arraylist/arraylist.test.bro index ae64e8d..8a9393a 100644 --- a/std/arraylist/arraylist.test.bro +++ b/std/arraylist/arraylist.test.bro @@ -7,6 +7,7 @@ handles_append test { try append(&list, 42) + try testing.expect_type(usize, list.items.len) try testing.expect_equal(1, list.items.len) try testing.expect_equal(42, list.items[0]) } diff --git a/std/testing/testing.bro b/std/testing/testing.bro index 3a05e90..3de655a 100644 --- a/std/testing/testing.bro +++ b/std/testing/testing.bro @@ -24,6 +24,10 @@ expect_equal func($T type, expected, actual T, location SourceLocation) void ! E } } +expect_type func($Expected, $Actual type, _ Actual, location SourceLocation) void ! Error { + try expect($(Expected == Actual), location) +} + run func(name []u8, callback *func() void ! Error) bool { callback() catch |_| { debug.print("{s} [failed]\n", {name,})