scalar family constraints for struct fields

This commit is contained in:
2026-07-13 23:00:42 +02:00
parent 6de4d9f9f3
commit 0eeacc2e37
3 changed files with 585 additions and 12 deletions
+217
View File
@@ -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)
}