diff --git a/LANGUAGE.md b/LANGUAGE.md index e7518a0..d3254bf 100644 --- a/LANGUAGE.md +++ b/LANGUAGE.md @@ -39,6 +39,20 @@ roadmap and milestone history. - native sum composition with `A | B` for unbacked enums and tagged unions, using program-global `u16` variant ids - fallible channel types `T ! E`, where `E` is a native enum/tagged union or supported sum composition +#### native record constraint fields + +A direct `int`, `float`, or `range` field in a named native struct or union is a +program-wide constraint, not per-value polymorphism. Before record layout, all reachable keyed +constructors, field assignments, and concrete uses of field reads contribute demands and the field +resolves once to one concrete runtime type. Compatible scalar demands widen normally. Integer +literals remain provisional until inference settles, so a later `usize` use can resolve an `int` +field to `usize`; otherwise literal-only `int` fields use the widest smallest-signed type required, +and literal-only `float` fields use `f64`. + +An undemanded field or incompatible demands are errors. This inference applies only to direct +fields of named native records. `c_struct` fields, nested constraints such as `[]int`, and fields in +anonymous generated records still require concrete runtime types. + #### keyword member names Reserved keywords are valid native enum members and tagged-union variants when used in an diff --git a/compiler/checker/checker.odin b/compiler/checker/checker.odin index e6f4f46..392fb86 100644 --- a/compiler/checker/checker.odin +++ b/compiler/checker/checker.odin @@ -174,6 +174,14 @@ Checker :: struct { global_open_float: []bool, global_const_value: []i128, global_demands_dirty: bool, + // Program-wide inference for direct constraint fields in named native records. + // The arrays use Store.fields' existing dense indices; resolved concrete types + // live directly in module.types.fields so layout and lowering need no side table. + record_field_constraints: []types.Type, + record_field_defaults: []types.Type, + record_field_conflicts: []types.Type, + record_field_conflict_spans: []source.Span, + record_field_demands_dirty: bool, external_global_canonical: []ast.Global_Id, external_global_diagnostics: []source.Diagnostic_Id, constants: []Constant, @@ -2399,6 +2407,211 @@ validate_declarations :: proc(checker: ^Checker) { } } +init_record_field_inference :: proc(checker: ^Checker) { + store := &checker.module.types + for item in store.nodes { + if !(item.declared && (item.kind == .Struct || item.kind == .Union) && + !item.c_layout && symbol.is_valid(symbol.Id(item.name))) { + continue + } + start := int(item.field_start) + end := start+int(item.field_count) + if start < 0 || end > len(store.fields) { + continue + } + for slot in start.. bool { + return slot >= 0 && slot < len(checker.record_field_constraints) && + types.is_constraint(checker.record_field_constraints[slot]) +} + +record_field_owner :: proc(checker: ^Checker, slot: int) -> (symbol.Id, symbol.Id, bool) { + for item in checker.module.types.nodes { + if !(item.declared && (item.kind == .Struct || item.kind == .Union) && + !item.c_layout && symbol.is_valid(symbol.Id(item.name))) { + continue + } + start := int(item.field_start) + if slot >= start && slot < start+int(item.field_count) && + slot >= 0 && slot < len(checker.module.types.fields) { + return symbol.Id(item.name), symbol.Id(checker.module.types.fields[slot].name), true + } + } + return symbol.INVALID, symbol.INVALID, false +} + +record_field_conflict :: proc(checker: ^Checker, slot: int, actual: types.Type, span: source.Span) { + if !is_inferred_record_field(checker, slot) || + types.is_valid(checker.record_field_conflicts[slot]) { + return + } + checker.record_field_conflicts[slot] = actual + checker.record_field_conflict_spans[slot] = span +} + +merge_record_field_default :: proc(checker: ^Checker, slot: int, candidate: types.Type, span: source.Span) -> bool { + if !is_inferred_record_field(checker, slot) || !is_runtime_type(checker, candidate) { + return false + } + constraint := checker.record_field_constraints[slot] + if !types.constraint_accepts(constraint, candidate, &checker.module.types) { + record_field_conflict(checker, slot, candidate, span) + return false + } + current := checker.record_field_defaults[slot] + if !is_runtime_type(checker, current) { + checker.record_field_defaults[slot] = candidate + checker.record_field_demands_dirty = true + return true + } + if types.equal(current, candidate) { + return false + } + merged := types.widest(current, candidate) + if types.is_concrete_scalar(merged) { + if !types.equal(current, merged) { + checker.record_field_defaults[slot] = merged + checker.record_field_demands_dirty = true + return true + } + return false + } + record_field_conflict(checker, slot, candidate, span) + return false +} + +merge_record_field_demand :: proc(checker: ^Checker, slot: int, demand: types.Type, span: source.Span) -> bool { + if !is_inferred_record_field(checker, slot) || !is_runtime_type(checker, demand) { + return false + } + constraint := checker.record_field_constraints[slot] + if !types.constraint_accepts(constraint, demand, &checker.module.types) { + record_field_conflict(checker, slot, demand, span) + return false + } + current := checker.module.types.fields[slot].type + if types.is_constraint(current) { + checker.module.types.fields[slot].type = demand + checker.record_field_demands_dirty = true + return true + } + if types.equal(current, demand) { + return false + } + merged := types.widest(current, demand) + if types.is_concrete_scalar(merged) { + if !types.equal(current, merged) { + checker.module.types.fields[slot].type = merged + checker.record_field_demands_dirty = true + return true + } + return false + } + record_field_conflict(checker, slot, demand, span) + return false +} + +record_field_expr_candidate :: proc( + checker: ^Checker, + slot: int, + expr_id: ast.Expr_Id, + inferred: types.Type, + locals: []Infer_Local, + pkg: ast.Package_Id, + file: ast.File_Id, +) -> bool { + if !is_inferred_record_field(checker, slot) || expr_id == ast.INVALID_EXPR { + return false + } + expr := checker.ast_module.exprs[expr_id] + constraint := checker.record_field_constraints[slot] + if constant := eval_integer_constant_in_context(checker, expr_id, pkg, file); + constant.kind == .Value && fits_i64(constant.value) { + candidate := types.smallest_signed_for_literal(i64(constant.value)) + if constraint == types.FLOAT { + candidate = types.F64 + } + return merge_record_field_default(checker, slot, candidate, expr.span) + } + if is_float_constant_expr(checker, expr_id) { + return merge_record_field_default(checker, slot, types.F64, expr.span) + } + if numeric_operand_is_open(checker, expr_id, locals, pkg, file) { + candidate := inferred + if constraint == types.FLOAT && types.is_concrete_integer(candidate) { + candidate = types.F64 + } + return merge_record_field_default(checker, slot, candidate, expr.span) + } + concrete := types.constraint_target(constraint, inferred, &checker.module.types) + if !is_runtime_type(checker, concrete) { + if is_runtime_type(checker, inferred) { + record_field_conflict(checker, slot, inferred, expr.span) + } + return false + } + return merge_record_field_demand(checker, slot, concrete, expr.span) +} + +finalize_record_field_inference :: proc(checker: ^Checker) { + for constraint, slot in checker.record_field_constraints { + if !types.is_constraint(constraint) { + continue + } + record_name, field_name, ok := record_field_owner(checker, slot) + if !ok { + continue + } + current := checker.module.types.fields[slot].type + conflict := checker.record_field_conflicts[slot] + if types.is_valid(conflict) { + if is_runtime_type(checker, current) { + source.addf( + checker.diagnostics, + checker.record_field_conflict_spans[slot], + "conflicting types %s and %s for field '%s.%s' declared as '%s'", + types.name(current), types.name(conflict), + symbol_text(checker, record_name), symbol_text(checker, field_name), + types.name(constraint), + ) + } else { + source.addf( + checker.diagnostics, + checker.record_field_conflict_spans[slot], + "type %s does not satisfy the '%s' constraint for field '%s.%s'", + types.name(conflict), types.name(constraint), + symbol_text(checker, record_name), symbol_text(checker, field_name), + ) + } + } else if types.is_constraint(current) { + source.addf( + checker.diagnostics, + source.Span{}, + "could not resolve the '%s' constraint for field '%s.%s'", + types.name(constraint), + symbol_text(checker, record_name), symbol_text(checker, field_name), + ) + } + if types.is_constraint(current) { + switch constraint { + case types.INT: + checker.module.types.fields[slot].type = types.I64 + case types.FLOAT: + checker.module.types.fields[slot].type = types.F64 + case types.RANGE: + checker.module.types.fields[slot].type = types.range(&checker.module.types, types.I64) + } + } + } +} + validate_type_nodes :: proc(checker: ^Checker) { for item, index in checker.module.types.nodes { id := types.DYNAMIC_START+types.Type(index) @@ -2459,9 +2672,14 @@ validate_type_nodes :: proc(checker: ^Checker) { // variant has no runtime value, only a tag. Allowed only here, not for // structs, untagged unions, or c_structs. tagged_union := item.kind == .Union && types.is_enum(item.child, &checker.module.types) - for field in types.fields_for(&checker.module.types, id) { + for field, field_index in types.fields_for(&checker.module.types, id) { + field_slot := int(item.field_start)+field_index if tagged_union && types.is_void(field.type) { // void variant: no payload to validate. + } else if is_inferred_record_field(checker, field_slot) { + // Direct constraints in named native records are validated after the + // program-wide inference fixpoint. C, nested, and anonymous fields do + // not enter this state and retain the existing validation below. } else if !types.is_runtime_value(field.type, &checker.module.types) { source.add( checker.diagnostics, @@ -2850,7 +3068,11 @@ infer_compound_expr :: proc( if types.is_pointer(value, store) { value = types.child_type(value, store) } - _, field, ok := find_struct_field(checker, value, expr.name) + slot, field, ok := find_struct_field_slot(checker, value, expr.name) + if ok && is_inferred_record_field(checker, slot) && is_runtime_type(checker, expected) { + _ = merge_record_field_demand(checker, slot, expected, expr.span) + field = checker.module.types.fields[slot] + } return field.type if ok else types.INVALID case .Unwrap: value := infer_nested_expr(checker, expr.left, locals, pkg, file, demanded, local_types) @@ -2887,15 +3109,37 @@ infer_compound_expr :: proc( resize(&block_locals, capture_start) return success case .Struct_Literal: + value := types.INVALID + if expr.left != ast.INVALID_EXPR { + value, _ = resolve_type_argument(checker, expr.left, pkg, file) + } else if symbol.is_valid(expr.name) { + target_pkg, available := expr_package(checker, expr, pkg, file) + value = types.find_named(store, u32(target_pkg), u32(expr.name), file=u32(expr_lookup_file(expr, file))) if available else types.INVALID + } else { + value = expected + } + value = types.resolve_alias(value, store) for keyed in expr.args { - _ = infer_nested_expr(checker, checker.ast_module.exprs[keyed].left, locals, pkg, file, demanded, local_types) + keyed_expr := checker.ast_module.exprs[keyed] + if keyed_expr.left == ast.INVALID_EXPR { + continue + } + slot, field, ok := find_struct_field_slot(checker, value, keyed_expr.name) + if !ok || !is_inferred_record_field(checker, slot) { + _ = infer_nested_expr(checker, keyed_expr.left, locals, pkg, file, demanded, local_types) + continue + } + field_expected := field.type if is_runtime_type(checker, field.type) else types.INVALID + actual := infer_nested_expr(checker, keyed_expr.left, locals, pkg, file, demanded, local_types, field_expected) + _ = record_field_expr_candidate(checker, slot, keyed_expr.left, actual, locals, pkg, file) + if is_runtime_type(checker, checker.module.types.fields[slot].type) { + _ = record_demand( + checker, keyed_expr.left, checker.module.types.fields[slot].type, + locals, local_types, pkg, file, + ) + } } - if !symbol.is_valid(expr.name) { - return types.INVALID - } - target_pkg, available := expr_package(checker, expr, pkg, file) - value := types.find_named(store, u32(target_pkg), u32(expr.name), file=u32(expr_lookup_file(expr, file))) if available else types.INVALID - return types.resolve_alias(value, store) + return value case .Keyed: return infer_nested_expr(checker, expr.left, locals, pkg, file, demanded, local_types) case: @@ -2996,8 +3240,12 @@ infer_expr :: proc( base_type = types.child_type(base_type, &checker.module.types) } if !types.is_valid(last) { - _, field, ok := find_struct_field(checker, base_type, expr.name) + slot, field, ok := find_struct_field_slot(checker, base_type, expr.name) if ok { + if is_inferred_record_field(checker, slot) && is_runtime_type(checker, frame.expected) { + _ = merge_record_field_demand(checker, slot, frame.expected, expr.span) + field = checker.module.types.fields[slot] + } last = field.type } } @@ -3020,8 +3268,12 @@ infer_expr :: proc( base_type = types.child_type(base_type, &checker.module.types) } if !types.is_valid(last) { - _, field, ok := find_struct_field(checker, base_type, expr.name) + slot, field, ok := find_struct_field_slot(checker, base_type, expr.name) if ok { + if is_inferred_record_field(checker, slot) && is_runtime_type(checker, frame.expected) { + _ = merge_record_field_demand(checker, slot, frame.expected, expr.span) + field = checker.module.types.fields[slot] + } last = field.type } } @@ -3575,6 +3827,16 @@ infer_statements :: proc( locals^[local_index].mutable { _ = merge_infer_local_type(checker, &locals^[local_index], value_type, local_types) } + } else if target_expr.kind == .Field || + target_expr.kind == .Name && symbol.is_valid(target_expr.qualifier) { + if slot, ok := inferred_record_field_slot_from_expr( + checker, target_expr, locals^[:], local_types, pkg, file, + ); ok { + _ = record_field_expr_candidate( + checker, slot, statement.expr, value_type, locals^[:], pkg, file, + ) + target_type = checker.module.types.fields[slot].type + } } if !rhs_is_arith { _ = record_demand(checker, statement.expr, target_type, locals^[:], local_types, pkg, file) @@ -4097,8 +4359,39 @@ record_demand_shallow :: proc( return record_demand(checker, expr_id, demand, locals, local_types, pkg, file) } +inferred_record_field_slot_from_expr :: proc( + checker: ^Checker, + expr: ast.Expr, + locals: []Infer_Local, + local_types: []types.Type, + pkg: ast.Package_Id, + file: ast.File_Id, +) -> (int, bool) { + base := types.INVALID + field_name := expr.name + if expr.kind == .Field { + base = infer_nested_expr(checker, expr.left, locals, pkg, file, nil, local_types) + } else if expr.kind == .Name && symbol.is_valid(expr.qualifier) && + find_import(checker, file, expr.qualifier) == ast.INVALID_IMPORT { + base = find_infer_local(locals, expr.qualifier) + if !types.is_valid(base) { + if global := find_global(checker, expr.qualifier, pkg, file); global != ast.INVALID_GLOBAL { + base = checker.global_types[global] + } + } + } else { + return -1, false + } + if types.is_pointer(base, &checker.module.types) { + base = types.child_type(base, &checker.module.types) + } + slot, _, ok := find_struct_field_slot(checker, base, field_name) + return slot, ok && is_inferred_record_field(checker, slot) +} + // record_demand pushes a concrete type demand onto open numeric slots reachable -// through bare names and numeric arithmetic. Calls remain a boundary (milestone 14.5). +// through bare names, inferred record fields, and numeric arithmetic. Calls remain +// a boundary (milestone 14.5). record_demand :: proc( checker: ^Checker, expr_id: ast.Expr_Id, @@ -4115,6 +4408,13 @@ record_demand :: proc( expr := checker.ast_module.exprs[expr_id] #partial switch expr.kind { case .Name: + if symbol.is_valid(expr.qualifier) { + if slot, ok := inferred_record_field_slot_from_expr( + checker, expr, locals, local_types, pkg, file, + ); ok { + return merge_record_field_demand(checker, slot, demand, expr.span) + } + } if !symbol.is_valid(expr.qualifier) { if index, ok := find_infer_local_index(locals, expr.name); ok { return merge_local_demand(checker, &locals[index], demand, local_types) @@ -4128,6 +4428,12 @@ record_demand :: proc( if global != ast.INVALID_GLOBAL { return merge_global_demand(checker, global, demand) } + case .Field: + if slot, ok := inferred_record_field_slot_from_expr( + checker, expr, locals, local_types, pkg, file, + ); ok { + return merge_record_field_demand(checker, slot, demand, expr.span) + } case .Negate: if types.is_signed(demand, checker.target) || types.is_float(demand, checker.target) { return record_demand(checker, expr.left, demand, locals, local_types, pkg, file) @@ -4201,6 +4507,7 @@ infer_all :: proc(checker: ^Checker) { for { changed := false checker.global_demands_dirty = false + checker.record_field_demands_dirty = false spec_count := len(checker.specs) // Backward demands: a global pushes its own (declared or already-resolved) type @@ -4286,6 +4593,9 @@ infer_all :: proc(checker: ^Checker) { if checker.global_demands_dirty { changed = true } + if checker.record_field_demands_dirty { + changed = true + } if !changed { if !defaults_applied { defaults_applied = true @@ -4304,6 +4614,15 @@ infer_all :: proc(checker: ^Checker) { defaulted = true } } + for fallback, slot in checker.record_field_defaults { + if !is_inferred_record_field(checker, slot) || + !types.is_constraint(checker.module.types.fields[slot].type) || + !is_runtime_type(checker, fallback) { + continue + } + checker.module.types.fields[slot].type = fallback + defaulted = true + } if defaulted { continue } @@ -4802,6 +5121,19 @@ find_struct_field :: proc(checker: ^Checker, struct_type: types.Type, name: symb return 0, {}, false } +find_struct_field_slot :: proc(checker: ^Checker, struct_type: types.Type, name: symbol.Id) -> (int, types.Field, bool) { + item, ok := types.node(&checker.module.types, struct_type) + if !ok || (item.kind != .Struct && item.kind != .Union) { + return -1, {}, false + } + for field, index in types.fields_for(&checker.module.types, struct_type) { + if field.name == u32(name) { + return int(item.field_start)+index, field, true + } + } + return -1, {}, false +} + field_type_from_value :: proc(checker: ^Checker, expr: ast.Expr, base_type: types.Type) -> types.Type { store := &checker.module.types field_name := symbol_text(checker, expr.name) @@ -9956,6 +10288,11 @@ check :: proc( checker.global_open_const = make([]bool, len(ast_module.globals), allocator) checker.global_open_float = make([]bool, len(ast_module.globals), allocator) checker.global_const_value = make([]i128, len(ast_module.globals), allocator) + checker.record_field_constraints = make([]types.Type, len(checker.module.types.fields), allocator) + checker.record_field_defaults = make([]types.Type, len(checker.module.types.fields), allocator) + checker.record_field_conflicts = make([]types.Type, len(checker.module.types.fields), allocator) + checker.record_field_conflict_spans = make([]source.Span, len(checker.module.types.fields), allocator) + init_record_field_inference(&checker) checker.external_global_canonical = make([]ast.Global_Id, len(ast_module.globals), allocator) checker.external_global_diagnostics = make([]source.Diagnostic_Id, len(ast_module.globals), allocator) for &canonical in checker.external_global_canonical { @@ -9983,6 +10320,10 @@ check :: proc( delete(checker.global_open_const, allocator) delete(checker.global_open_float, allocator) delete(checker.global_const_value, allocator) + delete(checker.record_field_constraints, allocator) + delete(checker.record_field_defaults, allocator) + delete(checker.record_field_conflicts, allocator) + delete(checker.record_field_conflict_spans, allocator) delete(checker.external_global_canonical, allocator) delete(checker.external_global_diagnostics, allocator) delete(checker.constants, allocator) @@ -10043,6 +10384,7 @@ check :: proc( validate_declarations(&checker) configure_io_main(&checker) infer_all(&checker) + finalize_record_field_inference(&checker) validate_external_globals(&checker) prune_specs(&checker) build_globals(&checker) diff --git a/compiler_tests.odin b/compiler_tests.odin index 0568f57..bb869cd 100644 --- a/compiler_tests.odin +++ b/compiler_tests.odin @@ -7463,6 +7463,7 @@ main func() void { value Bad(i32) = undefined; _ = &value } symbol.destroy_table(&symbols) source.destroy_diagnostics(&diagnostics) } + } @(test) @@ -7932,6 +7933,7 @@ parser_diagnoses_malformed_conditional_unwrap_captures_and_guards :: proc(t: ^te symbol.destroy_table(&symbols) source.destroy_diagnostics(&diagnostics) } + } @(test) @@ -10998,3 +11000,218 @@ main func() void { } testing.expect(t, found) } + +named_record_field_type :: proc( + module: ^hir.Module, + symbols: ^symbol.Table, + record_name, field_name: string, +) -> (types.Type, bool) { + record := types.find_named(&module.types, 0, u32(symbol.intern(symbols, record_name))) + if !types.is_record(record, &module.types) { + return types.INVALID, false + } + field_symbol := symbol.intern(symbols, field_name) + for field in types.fields_for(&module.types, record) { + if field.name == u32(field_symbol) { + return field.type, true + } + } + return types.INVALID, false +} + +@(test) +native_record_constraint_fields_resolve_program_wide :: proc(t: ^testing.T) { + text := `Token :: struct { start int } +Backward :: struct { start int } +Wide :: struct { value int } +Literal :: struct { value int } +Measurement :: struct { + ratio float + span range +} +Payload :: union { count int } + +take_usize func(value usize) usize { return value } + +exercise func(cursor usize, narrow i8, wider i16, count i32) i32 { + token Token = Token{start = cursor} + wide Wide = Wide{value = narrow} + wide.value = wider + small Literal = Literal{value = 1} + large Literal = Literal{value = 1000} + backward Backward = Backward{start = 1} + measurement Measurement = Measurement{ratio = 1.5, span = 0..3} + payload Payload = Payload{count = count} + _ = token.start + _ = wide.value + _ = small.value + _ = large.value + _ = take_usize(backward.start) + _ = measurement.ratio + _ = measurement.span + _ = payload.count + return 0 +} + +main func() i32 { return exercise(7, 1, 1000, 3) } +` + source_file := source.Source{path="record_constraints.bro", text=text} + diagnostics := source.init_diagnostics(&source_file) + defer source.destroy_diagnostics(&diagnostics) + symbols := symbol.init_table() + defer symbol.destroy_table(&symbols) + stream := lexer.lex(&source_file, &diagnostics, &symbols) + defer delete(stream.items) + ast_module := parser.parse(&stream, &source_file, &diagnostics) + defer ast.destroy_module(&ast_module) + hir_module := checker.check(&ast_module, &diagnostics, &symbols) + defer hir.destroy_module(&hir_module) + + token_start, token_ok := named_record_field_type(&hir_module, &symbols, "Token", "start") + backward_start, backward_ok := named_record_field_type(&hir_module, &symbols, "Backward", "start") + wide_value, wide_ok := named_record_field_type(&hir_module, &symbols, "Wide", "value") + literal_value, literal_ok := named_record_field_type(&hir_module, &symbols, "Literal", "value") + ratio, ratio_ok := named_record_field_type(&hir_module, &symbols, "Measurement", "ratio") + span, span_ok := named_record_field_type(&hir_module, &symbols, "Measurement", "span") + count, count_ok := named_record_field_type(&hir_module, &symbols, "Payload", "count") + + testing.expect_value(t, len(diagnostics.items), 0) + testing.expect(t, token_ok && backward_ok && wide_ok && literal_ok && ratio_ok && span_ok && count_ok) + testing.expect_value(t, token_start, types.USIZE) + testing.expect_value(t, backward_start, types.USIZE) + testing.expect_value(t, wide_value, types.I16) + testing.expect_value(t, literal_value, types.I16) + testing.expect_value(t, ratio, types.F64) + testing.expect(t, types.is_range(span, &hir_module.types)) + testing.expect_value(t, types.child_type(span, &hir_module.types), types.I8) + testing.expect_value(t, count, types.I32) +} + +@(test) +native_record_int_field_compiles_and_runs_as_usize :: proc(t: ^testing.T) { + directory := "/tmp/brolang-test-record-field-usize" + main_path := "/tmp/brolang-test-record-field-usize/main.bro" + output := "/tmp/brolang-test-record-field-usize-output" + text := `Token :: struct { start int } +main func() i32 { + cursor usize = 7 + token Token = Token{start = cursor} + if (token.start != cursor) return 1 + 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), 0) + state := run_executable(output) + testing.expect_value(t, state.exit_code, 0) +} + +@(test) +native_record_constraint_fields_report_unresolved_and_conflicting_demands :: proc(t: ^testing.T) { + cases := [2]struct { + text: string, + needle: string, + }{ + { + text = `Token :: struct { start int } +main func() void {} +`, + needle = "could not resolve the 'int' constraint for field 'Token.start'", + }, + { + text = `Token :: struct { start int } +use func(signed i32, unsigned usize) void { + a Token = Token{start = signed} + b Token = Token{start = unsigned} + _ = a + _ = b +} +main func() void { use(1, 2) } +`, + needle = "conflicting types i32 and usize for field 'Token.start' declared as 'int'", + }, + } + for test_case in cases { + source_file := source.Source{path="bad_record_constraint.bro", text=test_case.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) + ir_module := lower.lower(&hir_module) + llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols) + + matching := 0 + for diagnostic in diagnostics.items { + matching += 1 if strings.contains(diagnostic.message, test_case.needle) else 0 + } + testing.expect_value(t, matching, 1) + testing.expect(t, len(llvm_text) > 0) + + delete(llvm_text) + ir.destroy_module(&ir_module) + hir.destroy_module(&hir_module) + ast.destroy_module(&ast_module) + delete(stream.items) + symbol.destroy_table(&symbols) + source.destroy_diagnostics(&diagnostics) + } + + directory := "/tmp/brolang-test-record-field-conflict" + main_path := "/tmp/brolang-test-record-field-conflict/main.bro" + output := "/tmp/brolang-test-record-field-conflict-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)cases[1].text)) + testing.expect_value(t, compiler_core.compile_package(directory, output), 1) + state := run_executable(output) + testing.expect(t, !state.success) +} + +@(test) +constraint_fields_remain_rejected_outside_named_native_records :: proc(t: ^testing.T) { + text := `BadC :: c_struct { value int } +BadNested :: struct { values []int } + +make_type func() type { + return struct { value int } +} + +main func() void { + Generated :: @make_type() + _ = Generated +} +` + source_file := source.Source{path="excluded_record_constraints.bro", text=text} + diagnostics := source.init_diagnostics(&source_file) + defer source.destroy_diagnostics(&diagnostics) + symbols := symbol.init_table() + defer symbol.destroy_table(&symbols) + stream := lexer.lex(&source_file, &diagnostics, &symbols) + defer delete(stream.items) + ast_module := parser.parse(&stream, &source_file, &diagnostics) + defer ast.destroy_module(&ast_module) + hir_module := checker.check(&ast_module, &diagnostics, &symbols) + defer hir.destroy_module(&hir_module) + + record_errors := 0 + anonymous_error := false + for diagnostic in diagnostics.items { + record_errors += 1 if strings.contains(diagnostic.message, "record fields must have runtime value types") else 0 + anonymous_error = anonymous_error || strings.contains(diagnostic.message, "anonymous struct field 'value' requires a concrete runtime type") + } + c_field, c_ok := named_record_field_type(&hir_module, &symbols, "BadC", "value") + nested_field, nested_ok := named_record_field_type(&hir_module, &symbols, "BadNested", "values") + testing.expect_value(t, record_errors, 1) + testing.expect(t, anonymous_error) + testing.expect(t, c_ok && nested_ok) + testing.expect_value(t, c_field, types.INT) + testing.expect(t, types.is_slice(nested_field, &hir_module.types)) + testing.expect_value(t, types.child_type(nested_field, &hir_module.types), types.INT) +}