diff --git a/compiler/checker/checker.odin b/compiler/checker/checker.odin index fbdbda6..ffe359b 100644 --- a/compiler/checker/checker.odin +++ b/compiler/checker/checker.odin @@ -231,7 +231,6 @@ Checker :: struct { constants: []Constant, template_diagnostics: []source.Diagnostic_Id, constant_stack: [dynamic]Constant_Frame, - ast_expr_stack: [dynamic]ast.Expr_Id, hir_expr_stack: [dynamic]hir.Expr_Id, infer_stack: [dynamic]Infer_Frame, build_stack: [dynamic]Build_Expr_Frame, @@ -2090,6 +2089,9 @@ explicit_comptime_argument_valid :: proc( _, ok := resolve_type_argument(checker, arg, pkg, file) return ok } + if type_pattern_mentions_comptime(checker, function, comptime_param_count(function), param.type) { + return true + } if is_comptime_string_param(checker, param, function) { _, ok := comptime_string_argument(checker, arg, pkg, file) return ok @@ -2097,9 +2099,6 @@ 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 @@ -2307,6 +2306,38 @@ call_argument_mapping :: proc( } } } + if selected_index < 0 { + // Prefer the mapping that treats an array literal as a dependent aggregate. + // It can then report the contextual element error instead of an ambiguous + // hidden-comptime-parameter mapping failure. + dependent_array_candidate := -1 + ambiguous := false + for candidate, index in candidates { + matches := false + for arg_id, source_index in args { + param_index := call_param_index(candidate, source_index) + if param_index < 0 || param_index >= len(function.params) || + !function.params[param_index].comptime_value || + arg_id == ast.INVALID_EXPR || int(arg_id) >= len(checker.ast_module.exprs) || + checker.ast_module.exprs[arg_id].kind != .Array { + continue + } + matches = matches || type_pattern_mentions_comptime( + checker, function^, comptime_param_count(function^), function.params[param_index].type, + ) + } + if matches { + if dependent_array_candidate >= 0 { + ambiguous = true + } else { + dependent_array_candidate = index + } + } + } + if dependent_array_candidate >= 0 && !ambiguous { + selected_index = dependent_array_candidate + } + } if selected_index < 0 { if len(failures) > 0 { builder := strings.builder_make(checker.allocator) @@ -2638,7 +2669,10 @@ infer_call_comptime_values :: proc( continue } values[ordinal].name = param.name - if is_comptime_type_param(checker, param) { + dependent := type_pattern_mentions_comptime(checker, function, ordinal, param.type) + if dependent { + values[ordinal].kind = .Static + } else if is_comptime_type_param(checker, param) { values[ordinal].kind = .Type } else if is_comptime_string_param(checker, param, function) { values[ordinal].kind = .String @@ -2653,6 +2687,65 @@ infer_call_comptime_values :: proc( ordinal += 1 } matched := true + all_bound := true + for value_bound in bound { + all_bound = all_bound && value_bound + } + if !all_bound && is_runtime_type(checker, expected) { + if types.is_valid(function.error) && types.kind(expected, &checker.module.types) == .Fallible { + matched = match_inferred_type_pattern( + checker, function, prefix, function.result, + types.fallible_success(expected, &checker.module.types), values, bound, + source.Span{}, diagnose, + ) && matched + matched = match_inferred_type_pattern( + checker, function, prefix, function.error, + types.fallible_error(expected, &checker.module.types), values, bound, + source.Span{}, diagnose, + ) && matched + } else if type_pattern_mentions_comptime(checker, function, prefix, function.result) { + matched = match_inferred_type_pattern( + checker, function, prefix, function.result, expected, values, bound, + source.Span{}, diagnose, + ) && matched + } + } + // A comptime aggregate is evaluated only after its dependent type is known. + // Bind a direct `[N]T` parameter's N from an array literal's syntax first; + // this supplies the contextual element type for strings and enum literals. + for arg_id, source_index in args { + param_index := call_param_index(mapping, source_index) + if param_index < 0 || param_index >= len(function.params) || + !function.params[param_index].comptime_value || + arg_id == ast.INVALID_EXPR || int(arg_id) >= len(checker.ast_module.exprs) || + checker.ast_module.exprs[arg_id].kind != .Array { + continue + } + pattern, pattern_ok := types.node(&checker.module.types, function.params[param_index].type) + if !pattern_ok || pattern.kind != .Array || !pattern.unresolved_count || + pattern.count_expr == u32(ast.INVALID_EXPR) || int(pattern.count_expr) >= len(checker.ast_module.exprs) { + continue + } + count_expr := checker.ast_module.exprs[ast.Expr_Id(pattern.count_expr)] + if count_expr.kind != .Name || symbol.is_valid(count_expr.qualifier) { + continue + } + binding_index, is_binding := comptime_binding_index(function, prefix, count_expr.name) + if !is_binding || bound[binding_index] { + continue + } + param, param_ok := comptime_param_for_name(function, count_expr.name) + if !param_ok || is_comptime_type_param(checker, param) { + continue + } + values[binding_index] = Comptime_Value{ + name=count_expr.name, + type=type_from_syntax(checker, param.type, function.pkg, function.file), + value=i128(len(checker.ast_module.exprs[arg_id].args)), + kind=.Integer, + } + bound[binding_index] = true + } for arg_id, source_index in args { param_index := call_param_index(mapping, source_index) if param_index < 0 || param_index >= len(function.params) || !function.params[param_index].comptime_value || @@ -2669,6 +2762,7 @@ infer_call_comptime_values :: proc( matched = false continue } + dependent := type_pattern_mentions_comptime(checker, function, prefix, param.type) if is_comptime_type_param(checker, param) { actual, ok := resolve_type_argument(checker, arg_id, pkg, file) if !ok { @@ -2688,7 +2782,7 @@ infer_call_comptime_values :: proc( } values[binding_index] = Comptime_Value{name=param.name, type=actual, kind=.Type} bound[binding_index] = true - } else if is_comptime_string_param(checker, param, function) { + } else if !dependent && is_comptime_string_param(checker, param, function) { text, text_ok := comptime_string_argument(checker, arg_id, pkg, file) if !text_ok { if failure != nil && len(failure^) == 0 { @@ -2712,7 +2806,7 @@ infer_call_comptime_values :: proc( kind=.String, } bound[binding_index] = true - } else if types.is_concrete_integer(type_from_syntax(checker, param.type, function.pkg, function.file)) { + } else if !dependent && types.is_concrete_integer(type_from_syntax(checker, param.type, function.pkg, function.file)) { declared := type_from_syntax(checker, param.type, function.pkg, function.file) constant := eval_integer_constant_in_context(checker, arg_id, pkg, file) if constant.kind != .Value { @@ -2777,29 +2871,6 @@ infer_call_comptime_values :: proc( bound[binding_index] = true } } - all_bound := true - for value_bound in bound { - all_bound = all_bound && value_bound - } - if !all_bound && is_runtime_type(checker, expected) { - if types.is_valid(function.error) && types.kind(expected, &checker.module.types) == .Fallible { - matched = match_inferred_type_pattern( - checker, function, prefix, function.result, - types.fallible_success(expected, &checker.module.types), values, bound, - source.Span{}, diagnose, - ) && matched - matched = match_inferred_type_pattern( - checker, function, prefix, function.error, - types.fallible_error(expected, &checker.module.types), values, bound, - source.Span{}, diagnose, - ) && matched - } else if type_pattern_mentions_comptime(checker, function, prefix, function.result) { - matched = match_inferred_type_pattern( - checker, function, prefix, function.result, expected, values, bound, - source.Span{}, diagnose, - ) && matched - } - } all_bound = true for value_bound in bound { all_bound = all_bound && value_bound @@ -3461,12 +3532,9 @@ contains_name :: proc(names: []symbol.Id, name: symbol.Id) -> bool { } mark_expr_imports_used :: proc(checker: ^Checker, expr_id: ast.Expr_Id, file: ast.File_Id) { - stack := checker.ast_expr_stack - clear_dynamic_array(&stack) - defer { - clear_dynamic_array(&stack) - checker.ast_expr_stack = stack - } + stack: [dynamic]ast.Expr_Id + stack.allocator = checker.allocator + defer delete(stack) append(&stack, expr_id) for len(stack) > 0 { id := pop(&stack) @@ -3658,14 +3726,14 @@ validate_declarations :: proc(checker: ^Checker) { locals.allocator = checker.allocator comptime_prefix := 0 for param in function.params { + dependent := param.comptime_value && type_pattern_mentions_comptime( + checker, function, comptime_prefix, param.type, + ) param_type := types.INVALID - if param.comptime_value || !has_comptime { + if param.comptime_value && !dependent || !has_comptime { param_type = type_from_syntax(checker, param.type, function.pkg, function.file) } if param.comptime_value && !signature_poisoned { - dependent := type_pattern_mentions_comptime( - checker, function, comptime_prefix, param.type, type_params_only=true, - ) if function.c_abi { checker.template_diagnostics[function_id] = source.add( checker.diagnostics, @@ -4091,11 +4159,12 @@ validate_meta_schema :: proc(checker: ^Checker) { return types.find_named(&checker.module.types, u32(pkg), u32(symbol.intern(checker.symbols, name))) } field_info := find(checker, meta_package, "FieldInfo") + array_info := find(checker, meta_package, "ArrayInfo") record_info := find(checker, meta_package, "RecordInfo") enum_info := find(checker, meta_package, "EnumInfo") type_info := find(checker, meta_package, "TypeInfo") layout := find(checker, meta_package, "Layout") - valid := types.is_valid(field_info) && types.is_valid(record_info) && types.is_valid(enum_info) && + valid := types.is_valid(array_info) && types.is_valid(field_info) && types.is_valid(record_info) && types.is_valid(enum_info) && types.is_valid(type_info) && types.is_valid(layout) layout_item, layout_ok := types.node(&checker.module.types, layout) layout_members := types.enum_members_for(&checker.module.types, layout) @@ -4104,12 +4173,22 @@ validate_meta_schema :: proc(checker: ^Checker) { valid = symbol_text(checker, symbol.Id(layout_members[0].name)) == "auto" && symbol_text(checker, symbol.Id(layout_members[1].name)) == "c" } + array_item, array_ok := types.node(&checker.module.types, array_info) field_item, field_ok := types.node(&checker.module.types, field_info) record_item, record_ok := types.node(&checker.module.types, record_info) enum_item, enum_ok := types.node(&checker.module.types, enum_info) - valid = valid && field_ok && field_item.kind == .Struct && !field_item.tuple && !field_item.c_layout && + valid = valid && array_ok && array_item.kind == .Struct && !array_item.tuple && !array_item.c_layout && + field_ok && field_item.kind == .Struct && !field_item.tuple && !field_item.c_layout && record_ok && record_item.kind == .Struct && !record_item.tuple && !record_item.c_layout && enum_ok && enum_item.kind == .Struct && !enum_item.tuple && !enum_item.c_layout + array_fields := types.fields_for(&checker.module.types, array_info) + valid = valid && len(array_fields) == 2 + if valid { + valid = symbol_text(checker, symbol.Id(array_fields[0].name)) == "child" && + is_type_metatype_syntax(checker, array_fields[0].type) && + symbol_text(checker, symbol.Id(array_fields[1].name)) == "len" && + types.equal(array_fields[1].type, types.USIZE) + } field_fields := types.fields_for(&checker.module.types, field_info) valid = valid && len(field_fields) == 3 if valid { @@ -4151,9 +4230,10 @@ validate_meta_schema :: proc(checker: ^Checker) { for tag, index in expected_tags { field := type_fields[index] if symbol_text(checker, symbol.Id(field.name)) != tag || + (tag == "array" && !types.equal(field.type, array_info)) || (tag == "record" && !types.equal(field.type, record_info)) || (tag == "enum" && !types.equal(field.type, enum_info)) || - (tag != "record" && tag != "enum" && !types.is_void(field.type)) { + (tag != "array" && tag != "record" && tag != "enum" && !types.is_void(field.type)) { valid = false break } @@ -4242,7 +4322,7 @@ validate_type_nodes :: proc(checker: ^Checker) { } item_name := symbol_text(checker, symbol.Id(item.name)) comptime_meta := meta_package && - (item_name == "FieldInfo" || item_name == "RecordInfo" || item_name == "EnumInfo" || item_name == "TypeInfo") + (item_name == "ArrayInfo" || item_name == "FieldInfo" || item_name == "RecordInfo" || item_name == "EnumInfo" || item_name == "TypeInfo") if item.c_layout && !item.opaque && item.field_count == 0 { source.add( checker.diagnostics, @@ -14047,7 +14127,6 @@ check :: proc( append(&checker.module.strings, strings.clone(value, allocator)) } checker.constant_stack.allocator = allocator - checker.ast_expr_stack.allocator = allocator checker.hir_expr_stack.allocator = allocator checker.infer_stack.allocator = allocator checker.build_stack.allocator = allocator @@ -14120,7 +14199,6 @@ check :: proc( delete(checker.constants, allocator) delete(checker.template_diagnostics, allocator) delete(checker.constant_stack) - delete(checker.ast_expr_stack) delete(checker.hir_expr_stack) delete(checker.infer_stack) delete(checker.build_stack) @@ -14154,6 +14232,7 @@ check :: proc( delete(checker.comptime_keys) delete(checker.comptime_static_values) delete(checker.expand_context) + delete(checker.anon_globals) } for function, index in ast_module.functions { diff --git a/compiler/checker/comptime.odin b/compiler/checker/comptime.odin index 198695e..791ca97 100644 --- a/compiler/checker/comptime.odin +++ b/compiler/checker/comptime.odin @@ -325,6 +325,8 @@ Ct_State :: struct { diagnostic: source.Diagnostic_Id, silent: bool, demanded: ^[dynamic]Spec_Id, + promoted_cells: [dynamic]Ct_Cell_Id, + promoted_globals: [dynamic]hir.Global_Id, } Ct_Defer :: struct { @@ -359,6 +361,8 @@ ct_state_make :: proc( state.bindings.allocator = checker.allocator state.error_refinements.allocator = checker.allocator state.defers.allocator = checker.allocator + state.promoted_cells.allocator = checker.allocator + state.promoted_globals.allocator = checker.allocator for value in values { if value.kind == .Integer { id := ct_add_value(&state, Ct_Value{kind=.Integer, type=value.type, integer=value.value}) @@ -397,6 +401,8 @@ ct_state_destroy :: proc(state: ^Ct_State) { delete(state.bindings) delete(state.error_refinements) delete(state.defers) + delete(state.promoted_cells) + delete(state.promoted_globals) } ct_add_value :: proc(state: ^Ct_State, value: Ct_Value) -> Ct_Value_Id { @@ -703,7 +709,7 @@ ct_coerce_value :: proc(state: ^Ct_State, id: Ct_Value_Id, expected: types.Type, } return INVALID_CT_VALUE, ct_failf( state, .Not_Comptime, span, "cannot implicitly convert %s to %s at comptime", - types.name(value.type), types.name(expected), + type_label(state.checker, value.type), type_label(state.checker, expected), ) } @@ -841,6 +847,112 @@ ct_value_references_dead_storage :: proc(state: ^Ct_State, id: Ct_Value_Id) -> b return false } +ct_materialize_array_pointer :: proc( + state: ^Ct_State, + value: Ct_Value, + span: source.Span, +) -> (hir.Expr_Id, bool) { + checker := state.checker + store := &checker.module.types + pointer, pointer_ok := types.node(store, value.type) + place_id := Ct_Place_Id(value.index) + if !pointer_ok || pointer.kind != .Pointer || pointer.mutable || + place_id == INVALID_CT_PLACE || int(place_id) >= len(state.places) { + return hir.INVALID_EXPR, false + } + place := state.places[place_id] + if place.cell == INVALID_CT_CELL || int(place.cell) >= len(state.cells) || len(ct_place_path(state, place)) != 0 { + return hir.INVALID_EXPR, false + } + root_id := state.cells[place.cell].value + if root_id == INVALID_CT_VALUE || int(root_id) >= len(state.values) { + return hir.INVALID_EXPR, false + } + root := state.values[root_id] + array, array_ok := types.node(store, root.type) + if !array_ok || array.kind != .Array || + (pointer.many && (value.active != 0 || !types.equal(pointer.child, array.child))) || + (!pointer.many && (value.active != -1 || !types.equal(pointer.child, root.type))) { + return hir.INVALID_EXPR, false + } + global_id := hir.INVALID_GLOBAL + for cell, index in state.promoted_cells { + if cell == place.cell { + global_id = state.promoted_globals[index] + break + } + } + if global_id == hir.INVALID_GLOBAL { + root_expr := ct_materialize_value(state, root_id, span, root.type) + if root_expr == hir.INVALID_EXPR || expr_problematic(checker, root_expr) { + return hir.INVALID_EXPR, false + } + global_id = hir.Global_Id(len(checker.ast_module.globals)+len(checker.anon_globals)) + append(&checker.anon_globals, hir.Global{ + name=symbol.intern(checker.symbols, "__comptime.array"), + type=root.type, + expr=root_expr, + eager=true, + writable=false, + external=false, + diagnostic=source.INVALID_DIAGNOSTIC, + }) + append(&state.promoted_cells, place.cell) + append(&state.promoted_globals, global_id) + } + if checker.current_build_ctx != nil { + add_unique_global(checker.current_build_ctx.global_reads, global_id) + } + global := add_hir_expr(checker, hir.Expr{ + kind=.Global, span=span, type=root.type, target=hir.global_ref(global_id), + left=hir.INVALID_EXPR, right=hir.INVALID_EXPR, diagnostic=source.INVALID_DIAGNOSTIC, + }) + address := add_hir_expr(checker, hir.Expr{ + kind=.Address, span=span, type=types.pointer(store, root.type, false, false), left=global, + target=hir.INVALID_REF, right=hir.INVALID_EXPR, diagnostic=source.INVALID_DIAGNOSTIC, + }) + return coerce_expr(checker, address, value.type, span), true +} + +ct_materialize_array_slice :: proc( + state: ^Ct_State, + value: Ct_Value, + span: source.Span, +) -> (hir.Expr_Id, bool) { + store := &state.checker.module.types + slice, slice_ok := types.node(store, value.type) + place_id := Ct_Place_Id(value.index) + if !slice_ok || slice.kind != .Slice || slice.mutable || value.start != 0 || + place_id == INVALID_CT_PLACE || int(place_id) >= len(state.places) { + return hir.INVALID_EXPR, false + } + place := state.places[place_id] + if place.cell == INVALID_CT_CELL || int(place.cell) >= len(state.cells) || len(ct_place_path(state, place)) != 0 { + return hir.INVALID_EXPR, false + } + root_id := state.cells[place.cell].value + if root_id == INVALID_CT_VALUE || int(root_id) >= len(state.values) { + return hir.INVALID_EXPR, false + } + root := state.values[root_id] + array, array_ok := types.node(store, root.type) + if !array_ok || array.kind != .Array || u64(value.count) != array.count || + !types.equal(slice.child, array.child) { + return hir.INVALID_EXPR, false + } + pointer := Ct_Value{ + kind=.Pointer, + type=types.pointer(store, root.type, false, false), + index=value.index, + active=-1, + } + address, ok := ct_materialize_array_pointer(state, pointer, span) + if !ok { + return hir.INVALID_EXPR, false + } + return coerce_expr(state.checker, address, value.type, span), true +} + ct_materialize_value :: proc( state: ^Ct_State, id: Ct_Value_Id, @@ -893,11 +1005,13 @@ ct_materialize_value :: proc( diagnostic=source.INVALID_DIAGNOSTIC, }) case .String: - return add_hir_expr(checker, hir.Expr{ - kind=.String, span=span, type=value.type, integer=i64(value.index), + literal_type := string_literal_type(checker, value.index) + literal := add_hir_expr(checker, hir.Expr{ + kind=.String, span=span, type=literal_type, integer=i64(value.index), target=hir.INVALID_REF, left=hir.INVALID_EXPR, right=hir.INVALID_EXPR, diagnostic=source.INVALID_DIAGNOSTIC, }) + return coerce_expr(checker, literal, value.type, span) case .Range: children := ct_child_slice(state, value) args := make([]hir.Expr_Id, 2, checker.allocator) @@ -942,9 +1056,20 @@ ct_materialize_value :: proc( target=hir.INVALID_REF, left=hir.INVALID_EXPR, right=hir.INVALID_EXPR, diagnostic=source.INVALID_DIAGNOSTIC, }) - case .Pointer, .Slice: + case .Pointer: + if result, ok := ct_materialize_array_pointer(state, value, span); ok { + return result + } if state.diagnostic == source.INVALID_DIAGNOSTIC { - state.diagnostic = source.add(checker.diagnostics, span, "comptime storage pointers and slices cannot materialize as runtime memory") + state.diagnostic = source.add(checker.diagnostics, span, "only immutable pointers to whole comptime arrays can materialize as runtime memory") + } + return invalid_hir_expr(checker, span, state.diagnostic, value.type) + case .Slice: + if result, ok := ct_materialize_array_slice(state, value, span); ok { + return result + } + if state.diagnostic == source.INVALID_DIAGNOSTIC { + state.diagnostic = source.add(checker.diagnostics, span, "only immutable full-array comptime slices can materialize as runtime memory") } return invalid_hir_expr(checker, span, state.diagnostic, value.type) case .Function: @@ -1184,6 +1309,9 @@ ct_eval_expr :: proc( if !ok || flow.kind != .Normal { return INVALID_CT_VALUE, flow, ok } + if !symbol.is_valid(expr.name) { + return ct_eval_tuple_field_value(state, base_id, expr.integer, expr.span) + } return ct_eval_field_value(state, base_id, expr.name, expr.span) case .Index: index_id, index_flow, index_ok := ct_eval_expr(state, expr.right, types.USIZE, depth+1) @@ -1419,8 +1547,9 @@ ct_eval_array_expr :: proc(state: ^Ct_State, expr: ast.Expr, expected: types.Typ has_expected = false result_type = types.INVALID } - start := u32(len(state.children)) - for arg in expr.args { + values := make([]Ct_Value_Id, len(expr.args), checker.allocator) + defer delete(values, checker.allocator) + for arg, index in expr.args { value, flow, ok := ct_eval_expr(state, arg, element_type, depth+1) if !ok || flow.kind != .Normal { return INVALID_CT_VALUE, flow, ok @@ -1430,7 +1559,7 @@ ct_eval_array_expr :: proc(state: ^Ct_State, expr: ast.Expr, expected: types.Typ } else if !types.equal(element_type, state.values[value].type) { element_type = types.widest(element_type, state.values[value].type) } - append(&state.children, value) + values[index] = value } if !types.is_valid(element_type) { element_type = types.I64 @@ -1438,14 +1567,15 @@ ct_eval_array_expr :: proc(state: ^Ct_State, expr: ast.Expr, expected: types.Typ if !has_expected { result_type = types.array(store, element_type, u64(len(expr.args)), false) } - children := state.children[int(start):int(start)+len(expr.args)] - for &child in children { + for &child in values { coerced, ok := ct_coerce_value(state, child, element_type, expr.span) if !ok { return INVALID_CT_VALUE, ct_flow(.Normal), false } child = coerced } + start := u32(len(state.children)) + append(&state.children, ..values) return ct_add_value(state, Ct_Value{kind=.Array, type=result_type, start=start, count=u32(len(expr.args))}), ct_flow(.Normal), true } @@ -1460,8 +1590,6 @@ ct_eval_struct_expr :: proc(state: ^Ct_State, expr: ast.Expr, expected: types.Ty 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) - } else if expr.tuple { - struct_type = types.INVALID } else { struct_type = types.resolve_alias(expected, store) } @@ -2694,11 +2822,12 @@ ct_typeinfo_value :: proc(state: ^Ct_State, target: types.Type, span: source.Spa checker := state.checker store := &checker.module.types typeinfo_type := std_named_type(checker, "@std/meta", "TypeInfo") + arrayinfo_type := std_named_type(checker, "@std/meta", "ArrayInfo") fieldinfo_type := std_named_type(checker, "@std/meta", "FieldInfo") recordinfo_type := std_named_type(checker, "@std/meta", "RecordInfo") enuminfo_type := std_named_type(checker, "@std/meta", "EnumInfo") layout_type := std_named_type(checker, "@std/meta", "Layout") - if !types.is_valid(typeinfo_type) || !types.is_valid(fieldinfo_type) || + if !types.is_valid(typeinfo_type) || !types.is_valid(arrayinfo_type) || !types.is_valid(fieldinfo_type) || !types.is_valid(recordinfo_type) || !types.is_valid(enuminfo_type) || !types.is_valid(layout_type) { return INVALID_CT_VALUE, ct_flow(.Normal), ct_fail( state, .Not_Comptime, span, @@ -2738,6 +2867,16 @@ ct_typeinfo_value :: proc(state: ^Ct_State, target: types.Type, span: source.Spa if !variant_ok { return INVALID_CT_VALUE, ct_flow(.Normal), ct_fail(state, .Not_Comptime, span, "@std/meta TypeInfo is malformed") } + if tag == "array" { + child_value := ct_add_value(state, Ct_Value{kind=.Type, type=types.INVALID, index=u64(item.child)}) + len_value := ct_add_value(state, Ct_Value{kind=.Integer, type=types.USIZE, integer=i128(item.count)}) + array_value := ct_struct_value(state, arrayinfo_type, []Ct_Value_Id{child_value, len_value}) + payload_start := u32(len(state.children)) + append(&state.children, array_value) + return ct_add_value(state, Ct_Value{ + kind=.Struct, type=typeinfo_type, start=payload_start, count=1, active=i64(variant_index), + }), ct_flow(.Normal), true + } if tag == "enum" { members := types.enum_members_for(store, resolved) field_values := make([]Ct_Value_Id, len(members), checker.allocator) @@ -3659,7 +3798,7 @@ eval_static_comptime_value :: proc( values: []Comptime_Value = nil, diagnose := false, ) -> (Comptime_Value, bool) { - state := ct_state_make(checker, pkg, file, values=values, diagnose=false) + state := ct_state_make(checker, pkg, file, values=values, diagnose=diagnose) defer ct_state_destroy(&state) id, flow, ok := ct_eval_expr(&state, expr, declared, 0) if ok && flow.kind == .Normal { diff --git a/compiler/hir/hir.odin b/compiler/hir/hir.odin index 4f2d409..5bb38c3 100644 --- a/compiler/hir/hir.odin +++ b/compiler/hir/hir.odin @@ -271,6 +271,7 @@ Global :: struct { expr: Expr_Id, static_value: i64, is_static: bool, + eager: bool, external: bool, writable: bool, dependencies: [dynamic]Global_Id, diff --git a/compiler/ir/ir.odin b/compiler/ir/ir.odin index 53fb378..8869582 100644 --- a/compiler/ir/ir.odin +++ b/compiler/ir/ir.odin @@ -179,6 +179,7 @@ Global :: struct { link_name: string, type: types.Type, is_static: bool, + eager: bool, external: bool, writable: bool, static_value: i64, diff --git a/compiler/llvm/llvm.odin b/compiler/llvm/llvm.odin index 4cb4451..2c7b56e 100644 --- a/compiler/llvm/llvm.odin +++ b/compiler/llvm/llvm.odin @@ -2642,7 +2642,12 @@ emit_constructor :: proc(emitter: ^Emitter) { ) strings.write_string(&emitter.builder, "define internal void @bro.init() {\nentry:\n") for global, global_id in emitter.module.globals { - if !global.is_static && !global.external && !global.problematic { + if global.eager && !global.is_static && !global.external && !global.problematic { + fmt.sbprintf(&emitter.builder, " %%g%d = call %s @bro.get.%d()\n", global_id, llvm_type(global.type, &emitter.module.types), global_id) + } + } + for global, global_id in emitter.module.globals { + if !global.eager && !global.is_static && !global.external && !global.problematic { fmt.sbprintf(&emitter.builder, " %%g%d = call %s @bro.get.%d()\n", global_id, llvm_type(global.type, &emitter.module.types), global_id) } } diff --git a/compiler/lower/lower.odin b/compiler/lower/lower.odin index 1c33139..9524379 100644 --- a/compiler/lower/lower.odin +++ b/compiler/lower/lower.odin @@ -1844,6 +1844,7 @@ lower :: proc(hir_module: ^hir.Module, allocator := context.allocator) -> ir.Mod link_name=fmt.aprintf("%s", global.link_name, allocator=allocator), type=global.type, is_static=global.is_static, + eager=global.eager, external=global.external, writable=global.writable, static_value=global.static_value, diff --git a/compiler_tests.odin b/compiler_tests.odin index 5894bbe..7e7789c 100644 --- a/compiler_tests.odin +++ b/compiler_tests.odin @@ -4990,6 +4990,10 @@ missing func() i32 { if true { } } +escape_slice func() []i32 { + values [2]mut i32 = [1, 2] + return values[..] +} GLOBAL :: 1 main func() void { runtime i32 = 1 @@ -5000,6 +5004,30 @@ main func() void { yield callback() } _ = $&GLOBAL + _ = ${ + values [2]mut i32 = [1, 2] + pointer *mut i32 :: (&values).ptr + yield pointer + } + _ = ${ + values [2]mut i32 = [1, 2] + yield values[1..].ptr + } + _ = ${ + values [2]mut i32 = [1, 2] + yield &values[0] + } + _ = ${ + values [2]mut i32 = [1, 2] + view []mut i32 = values[..] + yield view + } + _ = ${ + values [2]mut i32 = [1, 2] + view []i32 = values[1..] + yield view + } + _ = $escape_slice() _ = $spin() _ = $missing() _ = ${ @@ -5021,7 +5049,9 @@ main func() void { found_runtime := false runtime_only_count := 0 - found_pointer := false + pointer_errors := 0 + slice_errors := 0 + found_expired := false found_quota := false found_missing := false found_yield := false @@ -5029,14 +5059,18 @@ main func() void { message := diagnostic.message found_runtime = found_runtime || strings.contains(message, "unresolved comptime value 'runtime'") runtime_only_count += 1 if strings.contains(message, "runtime-only") else 0 - found_pointer = found_pointer || strings.contains(message, "comptime storage pointers and slices cannot materialize as runtime memory") + pointer_errors += 1 if strings.contains(message, "only immutable pointers to whole comptime arrays can materialize as runtime memory") else 0 + slice_errors += 1 if strings.contains(message, "only immutable full-array comptime slices can materialize as runtime memory") else 0 + found_expired = found_expired || strings.contains(message, "expired storage") found_quota = found_quota || strings.contains(message, "comptime evaluation exceeded the step quota") found_missing = found_missing || strings.contains(message, "did not return a value") found_yield = found_yield || strings.contains(message, "comptime block must yield a value") } testing.expect(t, found_runtime) testing.expect(t, runtime_only_count >= 2) - testing.expect(t, found_pointer) + testing.expect(t, pointer_errors >= 4) + testing.expect(t, slice_errors >= 2) + testing.expect(t, found_expired) testing.expect(t, found_quota) testing.expect(t, found_missing) testing.expect(t, found_yield) @@ -14706,6 +14740,188 @@ std_meta_tests_compile_and_run :: proc(t: ^testing.T) { testing.expect_value(t, state.exit_code, 0) } +@(test) +array_reflection_reports_child_and_logical_length :: proc(t: ^testing.T) { + directory := "/tmp/brolang-test-array-reflection" + main_path := "/tmp/brolang-test-array-reflection/main.bro" + output := "/tmp/brolang-test-array-reflection-output" + text := `meta :: import "@std/meta" + +Alias :: alias [3]u16 + +matches func($Array, $Child type, $len usize) bool { + match typeinfo!(Array) { + .array |info|: return info.child == Child and info.len == len + else: return false + } +} + +main func() i32 { + if !$(matches([4]i32, i32, 4)) { return 1 } + if !$(matches([0]bool, bool, 0)) { return 2 } + if !$(matches(Alias, u16, 3)) { return 3 } + if !$(matches([2]mut i64, i64, 2)) { return 4 } + if !$(matches([2;0]u8, u8, 2)) { return 5 } + return 0 +} +` + _ = 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)text)) + testing.expect_value(t, compiler_core.compile_package( + directory, output, nil, target.DEFAULT, cimport.Options{}, ".", + ), 0) + state := run_executable(output) + testing.expect_value(t, state.exit_code, 0) +} + +@(test) +static_string_map_infers_array_size_and_preserves_promoted_backing :: proc(t: ^testing.T) { + directory := "/tmp/brolang-test-static-string-map" + main_path := "/tmp/brolang-test-static-string-map/main.bro" + output := "/tmp/brolang-test-static-string-map-output" + text := `std :: import "@std" +static_string_map :: import "@std/static_string_map" + +TokenKind :: enum { + keyword_if + keyword_else + keyword_for + keyword_return + keyword_while +} + +keywords std.StaticStringMap(TokenKind) = static_string_map.init([ + {"if", .keyword_if}, + {"else", .keyword_else}, + {"for", .keyword_for}, + {"return", .keyword_return}, +]) +fallback :: static_string_map.init(TokenKind, [ + {"while", .keyword_while}, +]) +empty std.StaticStringMap(TokenKind) = static_string_map.init([]) +numbers []i32 = ${ + values [3]mut i32 = [7, 8, 9] + yield values[..] +} + +main func() i32 { + if keywords.keys.len != 4 or keywords.values.len != 4 or keywords.len_indexes.len != 7 { return 1 } + if keywords.min_len != 2 or keywords.max_len != 6 { return 17 } + if empty.keys.len != 0 or empty.values.len != 0 or empty.len_indexes.len != 0 { return 18 } + if numbers.len != 3 or numbers[0] != 7 or numbers[2] != 9 { return 19 } + if static_string_map.get(&keywords, "if") |value| { + if value != TokenKind.keyword_if { return 2 } + } else { return 3 } + if static_string_map.get(&keywords, "else") |value| { + if value != TokenKind.keyword_else { return 4 } + } else { return 5 } + if static_string_map.get(&keywords, "for") |value| { + if value != TokenKind.keyword_for { return 6 } + } else { return 7 } + if static_string_map.get(&keywords, "return") |value| { + if value != TokenKind.keyword_return { return 8 } + } else { return 9 } + if static_string_map.get(&keywords, "no") |_| { return 10 } + if static_string_map.get(&keywords, "four") |_| { return 11 } + if static_string_map.get(&keywords, "x") |_| { return 12 } + if static_string_map.get(&keywords, "longer-than-any-key") |_| { return 13 } + if static_string_map.get(&empty, "if") |_| { return 14 } + if static_string_map.get(&fallback, "while") |value| { + if value != TokenKind.keyword_while { return 15 } + } else { return 16 } + return 0 +} +` + _ = 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)text)) + testing.expect_value(t, compiler_core.compile_package( + directory, output, nil, target.DEFAULT, cimport.Options{}, ".", + ), 0) + state := run_executable(output) + testing.expect_value(t, state.exit_code, 0) +} + +@(test) +static_string_map_reports_duplicate_and_malformed_entries :: proc(t: ^testing.T) { + directory := "/tmp/brolang-test-static-string-map-errors" + main_path := "/tmp/brolang-test-static-string-map-errors/main.bro" + _ = os2.remove_all(directory) + defer _ = os2.remove_all(directory) + testing.expect(t, os.make_directory(directory) == nil) + + duplicate_text := `std :: import "@std" +static_string_map :: import "@std/static_string_map" +TokenKind :: enum { keyword_if, keyword_else } +bad std.StaticStringMap(TokenKind) = static_string_map.init([ + {"if", .keyword_if}, + {"if", .keyword_else}, +]) +main func() void {} +` + testing.expect(t, os.write_entire_file(main_path, transmute([]byte)duplicate_text)) + { + sources := source.init_store() + defer source.destroy_store(&sources) + diagnostics := source.init_store_diagnostics(&sources) + defer source.destroy_diagnostics(&diagnostics) + symbols := symbol.init_table() + defer symbol.destroy_table(&symbols) + module, loaded := loader.load(directory, &sources, &diagnostics, &symbols, project_root_path=".") + defer ast.destroy_module(&module) + hir_module := checker.check(&module, &diagnostics, &symbols) + defer hir.destroy_module(&hir_module) + testing.expect(t, loaded) + found := false + for diagnostic in diagnostics.items { + found = found || strings.contains(diagnostic.message, "duplicate static string map key") + } + testing.expect(t, found) + } + + malformed_text := `std :: import "@std" +static_string_map :: import "@std/static_string_map" +TokenKind :: enum { keyword_if } +bad std.StaticStringMap(TokenKind) = static_string_map.init([ + {123, .keyword_if}, +]) +bad_value std.StaticStringMap(TokenKind) = static_string_map.init([ + {"if", "bad"}, +]) +main func() void {} +` + testing.expect(t, os.write_entire_file(main_path, transmute([]byte)malformed_text)) + { + sources := source.init_store() + defer source.destroy_store(&sources) + diagnostics := source.init_store_diagnostics(&sources) + defer source.destroy_diagnostics(&diagnostics) + symbols := symbol.init_table() + defer symbol.destroy_table(&symbols) + module, loaded := loader.load(directory, &sources, &diagnostics, &symbols, project_root_path=".") + defer ast.destroy_module(&module) + hir_module := checker.check(&module, &diagnostics, &symbols) + defer hir.destroy_module(&hir_module) + testing.expect(t, loaded) + found_key := false + found_value := false + for diagnostic in diagnostics.items { + found_key = found_key || strings.contains(diagnostic.message, "cannot implicitly convert i8 to []u8 at comptime") + found_value = found_value || + strings.contains(diagnostic.message, "cannot implicitly convert") && + strings.contains(diagnostic.message, "to TokenKind at comptime") + } + testing.expect(t, found_key) + testing.expect(t, found_value) + } +} + @(test) noreturn_functions_function_pointers_and_peer_types_compile_and_run :: proc(t: ^testing.T) { directory := "/tmp/brolang-test-noreturn" diff --git a/std/meta/meta.bro b/std/meta/meta.bro index 5780e89..2059242 100644 --- a/std/meta/meta.bro +++ b/std/meta/meta.bro @@ -1,6 +1,8 @@ -Layout :: enum { - auto - c +Layout :: enum { auto c } + +ArrayInfo :: struct { + child type + len usize } FieldInfo :: struct { @@ -28,7 +30,7 @@ TypeInfo :: union(enum) { bool void integer void float void - array void + array ArrayInfo pointer void slice void range void diff --git a/std/meta/meta.test.bro b/std/meta/meta.test.bro index a0d64b2..69a2e56 100644 --- a/std/meta/meta.test.bro +++ b/std/meta/meta.test.bro @@ -7,6 +7,22 @@ TestTokenKind :: enum(u8) { } TestNames :: alias EnumFieldStruct(TestTokenKind, ?[]u8, some!(null)) +TestArrayAlias :: alias [3]u16 + +hide array_info_matches func($Array, $Child type, $len usize) bool { + match typeinfo!(Array) { + .array |info|: return info.child == Child and info.len == len + else: return false + } +} + +array_reflection_exposes_child_and_logical_length test { + 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(TestArrayAlias, u16, 3))) + try testing.expect($(array_info_matches([2]mut i64, i64, 2))) + try testing.expect($(array_info_matches([2;0]u8, u8, 2))) +} enum_field_struct_defaults test { names TestNames = { diff --git a/std/static_string_map/static_string_map.bro b/std/static_string_map/static_string_map.bro new file mode 100644 index 0000000..9748b4a --- /dev/null +++ b/std/static_string_map/static_string_map.bro @@ -0,0 +1,109 @@ +import "@std/mem" + +StaticStringMap func($V type) type { + return struct { + keys [][]u8 + values []V + len_indexes []u32 + min_len u32 + max_len u32 + } +} + +hide Pair func($V type) type { + return struct { []u8, V } +} + +init func($V type, $N usize, $entries [N]Pair(V)) StaticStringMap(V) { + return ${ + if N > usize(maxval!(u32)) { + compile_error!("static string map has too many entries") + } + + keys [N]mut []u8 = undefined + values [N]mut V = undefined + for entries |entry, i| { + if entry.0.len > usize(maxval!(u32)) { + compile_error!("static string map key is too long") + } + for (usize(0))..i |prior| { + other :: entries[prior].0 + equal bool = entry.0.len == other.len + byte_index usize = 0 + while equal and byte_index < entry.0.len : byte_index += 1 { + equal = entry.0[byte_index] == other[byte_index] + } + if equal { + compile_error!("duplicate static string map key") + } + } + keys[i] = entry.0 + values[i] = entry.1 + } + + if N == 0 { + len_indexes [0]mut u32 = undefined + yield StaticStringMap(V) { + keys = keys[..], + values = values[..], + len_indexes = len_indexes[..], + min_len = 0, + max_len = 0, + } + } + + # ponytail: insertion sort is compile-time O(N²); replace if large maps affect builds. + i usize = 1 + while i < N : i += 1 { + key :: keys[i] + value :: values[i] + j usize = i + while j > 0 and keys[j - 1].len > key.len : j -= 1 { + keys[j] = keys[j - 1] + values[j] = values[j - 1] + } + keys[j] = key + values[j] = value + } + + min_len u32 :: u32(keys[0].len) + max_len u32 :: u32(keys[N - 1].len) + len_indexes [usize(max_len) + 1]mut u32 = undefined + entry_index usize = 0 + length usize = 0 + while length <= usize(max_len) : length += 1 { + while entry_index < N and keys[entry_index].len < length : entry_index += 1 {} + len_indexes[length] = u32(entry_index) + } + + yield StaticStringMap(V) { + keys = keys[..], + values = values[..], + len_indexes = len_indexes[..], + min_len = min_len, + max_len = max_len, + } + } +} + +get func($V type, map @StaticStringMap(V), key []u8) ?V { + if map.keys.len == 0 or key.len > usize(maxval!(u32)) { + return null + } + length u32 :: u32(key.len) + if length < map.min_len or length > map.max_len { + return null + } + index usize = usize(map.len_indexes[usize(length)]) + while index < map.keys.len { + candidate :: map.keys[index] + if candidate.len != key.len { + return null + } + if mem.eql(u8, candidate, key) { + return map.values[index] + } + index += 1 + } + return null +} diff --git a/std/static_string_map/static_string_map.test.bro b/std/static_string_map/static_string_map.test.bro new file mode 100644 index 0000000..d75aeb3 --- /dev/null +++ b/std/static_string_map/static_string_map.test.bro @@ -0,0 +1,36 @@ +import "@std/testing" + +TokenKind :: enum { + keyword_if + keyword_else + keyword_for + keyword_return +} + +keywords StaticStringMap(TokenKind) = init([ + {"return", .keyword_return}, + {"if", .keyword_if}, + {"for", .keyword_for}, + {"else", .keyword_else}, +]) + +handles_length_bucket_lookups test { + try testing.expect(keywords.keys.len == 4) + try testing.expect(keywords.values.len == keywords.keys.len) + try testing.expect(keywords.len_indexes.len == 7) + try testing.expect_equal(some!(TokenKind.keyword_if), get(&keywords, "if")) + try testing.expect_equal(some!(TokenKind.keyword_else), get(&keywords, "else")) + try testing.expect_equal(some!(TokenKind.keyword_for), get(&keywords, "for")) + try testing.expect_equal(some!(TokenKind.keyword_return), get(&keywords, "return")) + try testing.expect_equal(null, get(&keywords, "no")) + try testing.expect_equal(null, get(&keywords, "four")) + try testing.expect_equal(null, get(&keywords, "longer-than-any-key")) +} + +handles_empty_maps test { + empty StaticStringMap(TokenKind) = init([]) + try testing.expect(empty.keys.len == 0) + try testing.expect(empty.values.len == 0) + try testing.expect(empty.len_indexes.len == 0) + try testing.expect_equal(null, get(&empty, "if")) +} diff --git a/std/std.bro b/std/std.bro index 0da939b..5ea6cf9 100644 --- a/std/std.bro +++ b/std/std.bro @@ -1,7 +1,9 @@ import "io" import "enums" import "arraylist" +import "static_string_map" Io :: alias io.Io ArrayList :: alias arraylist.ArrayList EnumMap :: alias enums.EnumMap +StaticStringMap :: alias static_string_map.StaticStringMap