diff --git a/LANGUAGE.md b/LANGUAGE.md index b76510b..a9dbd2c 100644 --- a/LANGUAGE.md +++ b/LANGUAGE.md @@ -155,7 +155,7 @@ as `math.divfloor(a, b)` resolve to ordinary functions. - demand-monomorphized Brolang and C-ABI functions - integer comptime value parameters such as `make_array func($N usize) [N]u8`, specialized by value and omitted from the runtime ABI - explicit comptime type parameters such as `max func($T type, a, b T) T`, specialized by type and omitted from the runtime ABI -- 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 concrete function values; equal structural values and function declarations share specializations, while pointers, general slices, fallibles, ranges, untagged unions, and undefined values have no stable comptime identity +- 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 @@ -165,13 +165,15 @@ as `math.divfloor(a, b)` resolve to ordinary functions. - `tag!(value)` reads a tagged union's active discriminant and folds when the value is comptime-known; `tagname!(enum_value)` requires a comptime-known enum value and returns its immutable declaration name - bodyful `c_func` definitions and bodyless `c_func` declarations with exact external symbol names - concrete-only C signatures, C variadic declarations/calls, and C default argument promotions -- native function pointer values and types with `@func(...) R`, fallible `@func(...) R ! E`, optional `?@func(...) R`, comptime specialization by declaration identity, and non-variadic native indirect calls when the target is not statically known +- bare `func(...) R` and `c_func(...) R` values are comptime-only declaration identities; arrays, native records, optionals, and tagged unions containing one are also comptime-only and cannot enter runtime storage, ordinary ABI parameters/results, runtime globals, or C-layout records +- native function pointer values and types use `@func(...) R`, fallible `@func(...) R ! E`, and optional `?@func(...) R`; bare native identities implicitly materialize compatible pointers when a runtime pointer context requires one, but pointers never convert back to bare identities +- statically known bare identities and comptime-known pointers lower calls directly; native indirect calls remain available for runtime-selected non-variadic pointers - Apple Silicon C ABI lowering for scalars, pointers, fixed-signature plain records/unions, small aggregates, homogeneous float aggregates, and indirect aggregate returns - imported C typedefs, scalar constants, enum constants, fixed arrays, complete plain structs/unions, C `void*` as nullable `anyopaque` pointers, and pointers to opaque records - imported external C object variables, including mutable variables and immutable object globals - object-like scalar and plain record/union macro constants - supported static inline C functions through generated external wrappers -- C function pointer types, imported nullable callback typedefs, concrete `c_func` callback values, and postfix calls through non-null function pointers +- C function pointer types, imported nullable callback typedefs, bare `c_func` identities with one-way pointer materialization, and postfix calls through non-null function pointers - `brolang translate-c ... [--output-dir ]` for native `.bro` bindings from supported C declarations, with package-wide declaration deduplication when writing multiple headers - `brolang --translate-c stdio.h` for offline bindings from Zig-bundled standard C headers - ordered linking of additional C sources, objects, archives, library paths, and libraries through compiler CLI options diff --git a/README.md b/README.md index d7ac793..7948eb3 100644 --- a/README.md +++ b/README.md @@ -144,15 +144,20 @@ call_mapper func(mapper native.Imported_Mapper) c_int { } ``` -Native Brolang function pointer values use `*func(...) R`, with fallible +Native Brolang function pointer values use `@func(...) R`, with fallible channels written on the result: ```bro -call func(callback *func(value i32) i32, value i32) i32 { +call func(callback @func(value i32) i32, value i32) i32 { return callback(value) } ``` +Bare `func(...) R` and `c_func(...) R` values are comptime-only declaration +identities. They implicitly materialize compatible pointers in runtime pointer +contexts; pointers do not convert back to bare identities. Aggregates containing +bare identities are likewise comptime-only. + Bodyless manual and imported C functions may be variadic: ```bro @@ -226,11 +231,12 @@ Current prototype features: - Plain imported C structs/unions, fixed arrays, and C function pointer typedefs, including keyed literals, field access, callbacks, and Apple Silicon by-value ABI lowering - Qualified imported globals and functions with package-aware symbol mangling - Demand-monomorphized Brolang and C-ABI functions -- Recursively stable comptime values—including booleans, integers, floats, types, immutable bytes, enums, fixed arrays, records/tuples, optionals, tagged unions, and concrete function values—may be interleaved with runtime parameters, are erased from the ABI, and specialize from explicit arguments, declaration identity, or exact inference provenance +- Recursively stable comptime values—including booleans, integers, floats, types, immutable bytes, enums, fixed arrays, records/tuples, optionals, tagged unions, and bare function identities—may be interleaved with runtime parameters, are erased from the ABI, and specialize from explicit arguments, declaration identity, or exact inference provenance - Forced typed comptime expressions (`$sum(1, 2)`, `$Point { x = 1, y = 2 }`) and comptime value blocks (`${ yield 4 }`) - Zig-style comptime type factories returning anonymous native structs (`Box func($T type) type`, used as `Box(i32)`) - Comptime execution for bodyful Brolang functions with mutable locals, loops, `defer`/`errdefer`, `match`, `try`/`catch`, pointer/slice storage mutation, pointer captures, and calls through comptime-known function values -- Native function pointer values and types (`*func(...) R`, `*func(...) R ! E`, `?*func(...) R`) +- Comptime-only native and C function identities (`func(...) R`, `c_func(...) R`) with structural comptime-only propagation through aggregates +- Native function pointer values and types (`@func(...) R`, `@func(...) R ! E`, `?@func(...) R`) with implicit bare-to-pointer materialization - Typed allocation/reallocation through `std/mem` and generic dynamic arrays through `std/arraylist` - Bodyless concrete C function declarations with exact external symbol names - Bodyless manual and imported C variadic declarations with default argument promotions diff --git a/TODO.md b/TODO.md index c2e8b4c..4affae9 100644 --- a/TODO.md +++ b/TODO.md @@ -712,8 +712,10 @@ places, slicing, `.len`, `.ptr`, pointer captures, and pointer-param aliasing - comptime storage pointers/slices cannot materialize as runtime memory; escaped dead storage is rejected - - bare concrete non-comptime function names are values; native function pointer - types use `@func(...) R` and fallible `@func(...) R ! E` + - bare concrete non-comptime function names are comptime-only declaration identities; + native function pointer types use `@func(...) R` and fallible `@func(...) R ! E` + - bare identities implicitly materialize compatible runtime pointers, never the reverse; + aggregates containing bare identities remain comptime-only - comptime-known native/bodyful `c_func` values can be called; bodyless/imported callbacks remain runtime-only - native function pointers are non-variadic v1; C variadic function pointers stay @@ -914,12 +916,16 @@ - `isize` and `usize` remain concrete pointer-sized types 43. comptime function parameters (implemented) - - concrete native and C function values, function literals, and aggregates containing them - specialize by declaration identity rather than runtime address + - bare native and C function identities, function literals, and comptime-only aggregates + containing them specialize by declaration identity rather than runtime address - repeated declarations reuse specializations, distinct declarations specialize separately, and function-valued parameters remain erased from the runtime ABI - statically known callback invocations lower to direct calls; runtime-selected function pointers remain indirect, and bodyless C declarations remain runtime-only during comptime execution + - runtime pointer contexts implicitly materialize a bare identity; pointer-to-identity conversion is + rejected, and bare-containing aggregates cannot enter runtime storage or ABI/C layouts + +44. struct field defaults on declaration ## A word on unchecked casts diff --git a/compiler/checker/checker.odin b/compiler/checker/checker.odin index 32fd1e1..95fa45a 100644 --- a/compiler/checker/checker.odin +++ b/compiler/checker/checker.odin @@ -597,7 +597,8 @@ build_static_value :: proc(checker: ^Checker, value: Ct_Value, span: source.Span }) } if value.kind == .Function { - return build_function_value(checker, ast.Function_Id(u32(value.index)), span, expected) + value_expected := expected if types.is_valid(expected) else value.type + return build_function_value(checker, ast.Function_Id(u32(value.index)), span, value_expected) } if value.kind == .Array || value.kind == .Struct || value.kind == .Range { children := ct_child_slice(&checker.static_state, value) @@ -1174,6 +1175,38 @@ is_runtime_type :: proc(checker: ^Checker, value: types.Type) -> bool { return types.is_runtime_value(value, &checker.module.types) } +is_comptime_value_type :: proc(checker: ^Checker, value: types.Type, depth := 0) -> bool { + if depth > 256 || !types.is_valid(value) { + return false + } + if is_runtime_type(checker, value) { + return true + } + item, ok := types.node(&checker.module.types, value) + if !ok { + return false + } + if item.kind == .Function { + return true + } + if item.kind == .Array || item.kind == .Optional || item.kind == .Alias || item.kind == .Distinct { + return is_comptime_value_type(checker, item.child, depth+1) + } + if item.kind == .Struct || item.kind == .Union { + if !item.declared || item.opaque || item.c_layout || + (item.kind == .Union && !types.is_tagged_union(value, &checker.module.types)) { + return false + } + for field in types.fields_for(&checker.module.types, value) { + if !types.is_void(field.type) && !is_comptime_value_type(checker, field.type, depth+1) { + return false + } + } + return true + } + return false +} + is_undefined_expr :: proc(checker: ^Checker, expr_id: ast.Expr_Id) -> bool { if expr_id == ast.INVALID_EXPR || int(expr_id) >= len(checker.ast_module.exprs) { return false @@ -2006,7 +2039,7 @@ call_mapping_semantically_valid :: proc( break } } - if !is_runtime_type(checker, actual) { + if !is_runtime_type(checker, actual) && !can_implicitly_convert_type(checker, actual, declared) { return false, fmt.aprintf( "argument %d is not a runtime value", source_index+1, allocator=checker.allocator, @@ -2814,7 +2847,7 @@ resolve_generated_struct_type :: proc(checker: ^Checker, expr_id: ast.Expr_Id, p defer delete(fields, checker.allocator) for field, index in template_fields { resolved := type_from_syntax(checker, field.type, pkg, file) - if !is_runtime_type(checker, resolved) || types.is_void(resolved) { + if (!is_runtime_type(checker, resolved) && !is_comptime_value_type(checker, resolved)) || types.is_void(resolved) { if expr.tuple { source.addf(checker.diagnostics, expr.span, "tuple element %d requires a concrete runtime type, got %s", index, type_label(checker, resolved)) } else { @@ -3112,7 +3145,7 @@ function_value_signature :: proc( return params, result, true } -function_pointer_type_for_template :: proc( +function_type_for_template :: proc( checker: ^Checker, template: ast.Function_Id, demanded: ^[dynamic]Spec_Id = nil, @@ -3125,7 +3158,6 @@ function_pointer_type_for_template :: proc( defer delete(params, checker.allocator) function := checker.ast_module.functions[template] function_type := types.function(&checker.module.types, params, result, function.c_abi, function.variadic) - pointer_type := types.pointer(&checker.module.types, function_type, false, false) spec := INVALID_SPEC if demanded == nil { if demand_spec { @@ -3137,7 +3169,40 @@ function_pointer_type_for_template :: proc( spec = find_spec(checker, template, params) mark_spec_demanded(checker, spec, demanded) } - return pointer_type, spec, spec != INVALID_SPEC || !demand_spec + return function_type, spec, spec != INVALID_SPEC || !demand_spec +} + +function_pointer_type_for_template :: proc( + checker: ^Checker, + template: ast.Function_Id, + demanded: ^[dynamic]Spec_Id = nil, + demand_spec := true, +) -> (types.Type, Spec_Id, bool) { + function_type, spec, ok := function_type_for_template(checker, template, demanded, demand_spec) + if !ok { + return types.INVALID, spec, false + } + return types.pointer(&checker.module.types, function_type, false, false), spec, true +} + +function_expr_type_for_template :: proc( + checker: ^Checker, + template: ast.Function_Id, + expected: types.Type, + demanded: ^[dynamic]Spec_Id = nil, +) -> (types.Type, bool) { + function_type, _, ok := function_type_for_template(checker, template, demanded) + if !ok { + return types.INVALID, false + } + pointer_expected := expected + if types.is_optional(pointer_expected, &checker.module.types) { + pointer_expected = types.child_type(pointer_expected, &checker.module.types) + } + if types.can_coerce_function_pointer(function_type, pointer_expected, &checker.module.types) { + return pointer_expected, true + } + return function_type, true } contains_name :: proc(names: []symbol.Id, name: symbol.Id) -> bool { @@ -3358,7 +3423,7 @@ validate_declarations :: proc(checker: ^Checker) { ) } if !is_type_metatype_syntax(checker, param.type) && - !is_runtime_type(checker, param_type) && param_type != types.RANGE { + !is_comptime_value_type(checker, param_type) && param_type != types.RANGE { checker.template_diagnostics[function_id] = source.addf( checker.diagnostics, param.span, @@ -3366,6 +3431,13 @@ validate_declarations :: proc(checker: ^Checker) { symbol_text(checker, param.name), ) } + } else if !has_comptime && !signature_poisoned && types.is_comptime_only(param_type, &checker.module.types) { + checker.template_diagnostics[function_id] = source.addf( + checker.diagnostics, + param.span, + "parameter '%s' has a comptime-only type; prefix it with '$'", + symbol_text(checker, param.name), + ) } else if !has_comptime && !signature_poisoned { if diagnostic := add_unsupported_type_diagnostic(checker, param.span, param_type); diagnostic != source.INVALID_DIAGNOSTIC { @@ -3904,7 +3976,14 @@ validate_type_nodes :: proc(checker: ^Checker) { // not enter this state and retain the existing validation below. } else if comptime_meta { // Reflection metadata is compile-time-only and may contain `type`. - } else if !types.is_runtime_value(field.type, &checker.module.types) { + } else if item.c_layout && !types.is_runtime_value(field.type, &checker.module.types) { + source.add( + checker.diagnostics, + source.Span{}, + "c_struct fields must have C-layout-compatible types", + ) + } else if !types.is_runtime_value(field.type, &checker.module.types) && + !is_comptime_value_type(checker, field.type) { source.add( checker.diagnostics, source.Span{}, @@ -4505,8 +4584,8 @@ infer_expr :: proc( _ = pop(&stack) case .Function_Literal: template := ast.Function_Id(u32(expr.integer)) - pointer_type, _, ok := function_pointer_type_for_template(checker, template, demanded) - last = pointer_type if ok else types.INVALID + function_type, ok := function_expr_type_for_template(checker, template, frame.expected, demanded) + last = function_type if ok else types.INVALID _ = pop(&stack) case .Name: last = types.INVALID @@ -4614,9 +4693,9 @@ infer_expr :: proc( if template != ast.INVALID_FUNCTION && len(checker.ast_module.functions[template].unsupported_reason) == 0 && checker.template_diagnostics[template] == source.INVALID_DIAGNOSTIC { - pointer_type, _, ok := function_pointer_type_for_template(checker, template, demanded) + function_type, ok := function_expr_type_for_template(checker, template, frame.expected, demanded) if ok { - last = pointer_type + last = function_type } } } @@ -4631,7 +4710,7 @@ infer_expr :: proc( case .Call: if expr.left != ast.INVALID_EXPR { callee_type := infer_nested_expr(checker, expr.left, locals, pkg, file, demanded, local_types) - _, function_item, function_type, ok := types.function_pointer(callee_type, &checker.module.types) + _, function_item, function_type, ok := types.callable_function(callee_type, &checker.module.types) if !ok { last = types.INVALID _ = pop(&stack) @@ -4732,7 +4811,7 @@ infer_expr :: proc( continue } if callee_type, handled := infer_qualified_value_field_type(checker, expr, locals, pkg, file, demanded); handled { - _, function_item, function_type, ok := types.function_pointer(callee_type, &checker.module.types) + _, function_item, function_type, ok := types.callable_function(callee_type, &checker.module.types) if !ok { last = types.INVALID _ = pop(&stack) @@ -4760,6 +4839,19 @@ infer_expr :: proc( callee_type := types.INVALID if !symbol.is_valid(expr.qualifier) { callee_type = find_infer_local(locals, expr.name) + if !types.is_valid(callee_type) { + if binding, ok := current_static_binding(checker, expr.name); ok { + callee_type = binding.type + if binding.value != INVALID_CT_VALUE && int(binding.value) < len(checker.static_state.values) { + function_value := checker.static_state.values[binding.value] + if function_value.kind == .Function { + _, _, _ = function_pointer_type_for_template( + checker, ast.Function_Id(u32(function_value.index)), demanded, + ) + } + } + } + } if !types.is_valid(callee_type) { if value, ok := current_comptime_value(checker, expr.name); ok && value.kind == .Static { callee_type = value.type @@ -4780,7 +4872,7 @@ infer_expr :: proc( callee_type = checker.global_types[global] } } - _, function_item, function_type, ok := types.function_pointer(callee_type, &checker.module.types) + _, function_item, function_type, ok := types.callable_function(callee_type, &checker.module.types) if !ok { distinct_type := types.find_named(&checker.module.types, u32(target_pkg), u32(expr.name), file=u32(expr_lookup_file(expr, file))) distinct_item, distinct_ok := types.node(&checker.module.types, distinct_type) @@ -6269,6 +6361,7 @@ can_implicitly_convert_type :: proc(checker: ^Checker, actual, expected: types.T types.can_coerce_c_integer(actual, expected, checker.target) || types.can_coerce_c_scalar(actual, expected, checker.target) || types.can_weaken_pointer(actual, expected, store) || + types.can_coerce_function_pointer(actual, expected, store) || types.can_weaken_slice(actual, expected, store) || types.can_decay_slice_c_string(actual, expected, store) || types.can_decay_array_pointer(actual, expected, store) || @@ -6297,6 +6390,13 @@ coerce_expr :: proc( if types.equal(actual, expected) { return expr_id } + if types.can_coerce_function_pointer(actual, expected, &checker.module.types) { + expr := checker.module.exprs[expr_id] + if expr.kind == .Function { + expr.type = expected + return add_hir_expr(checker, expr) + } + } if types.can_weaken_pointer(actual, expected, &checker.module.types) { return add_hir_expr(checker, hir.Expr{ kind=.Weaken_Pointer, @@ -6355,6 +6455,7 @@ coerce_expr :: proc( if types.is_optional(expected, &checker.module.types) { child := types.child_type(expected, &checker.module.types) if types.equal(actual, child) || + types.can_coerce_function_pointer(actual, child, &checker.module.types) || types.can_widen(actual, child) || types.can_coerce_c_integer(actual, child, checker.target) || types.can_coerce_c_scalar(actual, child, checker.target) || @@ -6933,14 +7034,16 @@ build_function_value :: proc( } defer delete(params, checker.allocator) function_type := types.function(&checker.module.types, params, result, function.c_abi, function.variadic) - pointer_type := types.pointer(&checker.module.types, function_type, false, false) + value_type := function_type expected_pointer := expected if types.is_optional(expected_pointer, &checker.module.types) { expected_pointer = types.child_type(expected_pointer, &checker.module.types) } if _, _, expected_function, ok := types.function_pointer(expected_pointer, &checker.module.types); ok && types.equal(expected_function, function_type) { - pointer_type = expected_pointer + value_type = expected_pointer + } else if types.equal(expected, function_type) { + value_type = expected } spec := find_spec(checker, template, params) if spec == INVALID_SPEC { @@ -6950,14 +7053,14 @@ build_function_value :: proc( "could not resolve callback specialization of '%s'", symbol_text(checker, function.name), ) - return invalid_hir_expr(checker, span, id, pointer_type) + return invalid_hir_expr(checker, span, id, value_type) } function_id := checker.specs[spec].hir_id assert(function_id != hir.INVALID_FUNCTION) return add_hir_expr(checker, hir.Expr{ kind=.Function, span=span, - type=pointer_type, + type=value_type, target=hir.function_ref(function_id), left=hir.INVALID_EXPR, right=hir.INVALID_EXPR, @@ -7917,6 +8020,15 @@ build_expr :: proc( } expr := checker.ast_module.exprs[frame.expr] if frame.stage == 0 { + if expr.kind == .Name || expr.kind == .Field || expr.kind == .Index || expr.kind == .Unwrap { + if specialized, ok := try_build_specialization_expr( + checker, frame.expr, frame.expected, pkg, file, + ); ok { + last = specialized + _ = pop(&stack) + continue + } + } constant := Constant{} _, static_name := current_static_binding(checker, expr.name) if expr.kind != .Name || symbol.is_valid(expr.qualifier) || !static_name { @@ -8257,12 +8369,20 @@ build_expr :: proc( non_callable_global := false if !symbol.is_valid(expr.qualifier) { if local, ok := find_build_local(locals, expr.name); ok { - if _, _, _, callable := types.function_pointer(local.type, &checker.module.types); callable { + if _, _, _, callable := types.callable_function(local.type, &checker.module.types); callable { callee = build_local_expr(checker, local, expr.span) } else { non_callable = true } } + if callee == hir.INVALID_EXPR && !non_callable { + if binding, ok := current_static_binding(checker, expr.name); ok && + binding.value != INVALID_CT_VALUE && int(binding.value) < len(checker.static_state.values) { + callee = build_static_value( + checker, checker.static_state.values[binding.value], expr.span, binding.type, + ) + } + } if callee == hir.INVALID_EXPR && !non_callable { if value, ok := current_comptime_value(checker, expr.name); ok && value.kind == .Static && value.static_value != INVALID_CT_VALUE && @@ -8275,7 +8395,7 @@ build_expr :: proc( } if callee == hir.INVALID_EXPR && !non_callable { if global := find_global(checker, expr.name, target_pkg, expr_lookup_file(expr, file)); global != ast.INVALID_GLOBAL { - if _, _, _, callable := types.function_pointer(checker.global_types[global], &checker.module.types); callable { + if _, _, _, callable := types.callable_function(checker.global_types[global], &checker.module.types); callable { callee = build_global_reference(checker, global, expr.span, global_reads) } else { non_callable = true @@ -8322,7 +8442,7 @@ build_expr :: proc( id := source.INVALID_DIAGNOSTIC if non_callable { id = add_call_resolution_diagnostic(checker, expr, target_pkg, file) if non_callable_global else - source.add(checker.diagnostics, expr.span, "call target is not a function pointer") + source.add(checker.diagnostics, expr.span, "call target is not callable") } else { id = add_unsupported_diagnostic(checker, expr.span, target_pkg, expr.name) if id == source.INVALID_DIAGNOSTIC { @@ -8333,10 +8453,10 @@ build_expr :: proc( _ = pop(&stack) continue } - _, function_item, function_type, _ := types.function_pointer(checker.module.exprs[callee].type, &checker.module.types) + _, function_item, function_type, _ := types.callable_function(checker.module.exprs[callee].type, &checker.module.types) if !valid_callable_arity(function_item, len(expr.args)) { - message := "function pointer expects at least %d arguments, got %d" if function_item.variadic else - "function pointer expects %d arguments, got %d" + message := "function expects at least %d arguments, got %d" if function_item.variadic else + "function expects %d arguments, got %d" id := source.addf(checker.diagnostics, expr.span, message, function_item.field_count, len(expr.args)) last = invalid_hir_expr(checker, expr.span, id) _ = pop(&stack) @@ -8705,16 +8825,16 @@ build_expr :: proc( _ = pop(&stack) continue } - _, function_item, function_type, ok := types.function_pointer(checker.module.exprs[callee].type, &checker.module.types) + _, function_item, function_type, ok := types.callable_function(checker.module.exprs[callee].type, &checker.module.types) if !ok { - id := source.add(checker.diagnostics, expr.span, "call target is not a function pointer") + id := source.add(checker.diagnostics, expr.span, "call target is not callable") last = invalid_hir_expr(checker, expr.span, id) _ = pop(&stack) continue } if !valid_callable_arity(function_item, len(expr.args)) { - message := "function pointer expects at least %d arguments, got %d" if function_item.variadic else - "function pointer expects %d arguments, got %d" + message := "function expects at least %d arguments, got %d" if function_item.variadic else + "function expects %d arguments, got %d" id := source.addf(checker.diagnostics, expr.span, message, function_item.field_count, len(expr.args)) last = invalid_hir_expr(checker, expr.span, id) _ = pop(&stack) @@ -8739,7 +8859,7 @@ build_expr :: proc( if frame.arg_index+1 < len(expr.args) { next := frame.arg_index+1 callee_type := checker.module.exprs[frame.left].type - _, function_item, function_type, _ := types.function_pointer(callee_type, &checker.module.types) + _, function_item, function_type, _ := types.callable_function(callee_type, &checker.module.types) arg_expected := callable_arg_expected(function_type, function_item, &checker.module.types, next) if !is_runtime_type(checker, arg_expected) { arg_expected = types.INVALID @@ -8760,9 +8880,9 @@ build_expr :: proc( continue } callee_type := checker.module.exprs[frame.left].type - _, function_item, function_type, ok := types.function_pointer(callee_type, &checker.module.types) + _, function_item, function_type, ok := types.callable_function(callee_type, &checker.module.types) if !ok { - id := source.add(checker.diagnostics, expr.span, "call target is not a function pointer") + id := source.add(checker.diagnostics, expr.span, "call target is not callable") delete(stack[frame_index].built_args, checker.allocator) stack[frame_index].built_args = nil last = invalid_hir_expr(checker, expr.span, id) @@ -8789,7 +8909,7 @@ build_expr :: proc( } result := function_item.child if !types.is_valid(result) { - id := source.add(checker.diagnostics, expr.span, "could not resolve function pointer result type") + id := source.add(checker.diagnostics, expr.span, "could not resolve callable result type") delete(stack[frame_index].built_args, checker.allocator) stack[frame_index].built_args = nil last = invalid_hir_expr(checker, expr.span, id) @@ -9524,6 +9644,21 @@ build_block :: proc( value_type = types.INVALID } } + if types.is_comptime_only(value_type, &checker.module.types) { + id := source.addf( + checker.diagnostics, + statement.span, + "local '%s' has a comptime-only type and cannot be stored at runtime", + symbol_text(checker, statement.name), + ) + append(&body, hir.stmt_id(len(checker.module.statements))) + append(&checker.module.statements, hir.Stmt{ + kind = .Trap, span = statement.span, expr = hir.INVALID_EXPR, + local = hir.INVALID_LOCAL, diagnostic = id, + }) + ctx.problematic^ = true + continue + } if _, found := find_build_local(ctx.locals^[duplicate_start:], statement.name); found { id := source.addf( checker.diagnostics, statement.span, @@ -12145,6 +12280,14 @@ build_function :: proc(checker: ^Checker, id: Spec_Id) { signature_diagnostic := source.INVALID_DIAGNOSTIC unresolved_result := !types.is_void(spec.result) && !is_runtime_type(checker, spec.result) if unresolved_result { + if types.is_comptime_only(spec.result, &checker.module.types) { + signature_diagnostic = source.addf( + checker.diagnostics, + function.span, + "function '%s' has a comptime-only result and cannot return it through the runtime ABI", + symbol_text(checker, function.name), + ) + } checker.specs[id].result = types.I64 spec.result = types.I64 } @@ -12554,12 +12697,22 @@ build_globals :: proc(checker: ^Checker) { expr = invalid_hir_expr(checker, global.span, diagnostic, global_type) } if diagnostic == source.INVALID_DIAGNOSTIC && !is_runtime_type(checker, global_type) { - diagnostic = source.addf( - checker.diagnostics, - global.span, - "could not resolve a concrete type for global '%s'", - symbol_text(checker, global.name), - ) + if types.is_comptime_only(global_type, &checker.module.types) || + types.is_comptime_only(declared, &checker.module.types) { + diagnostic = source.addf( + checker.diagnostics, + global.span, + "global '%s' has a comptime-only type and cannot be stored at runtime", + symbol_text(checker, global.name), + ) + } else { + diagnostic = source.addf( + checker.diagnostics, + global.span, + "could not resolve a concrete type for global '%s'", + symbol_text(checker, global.name), + ) + } global_type = types.I64 expr = invalid_hir_expr(checker, global.span, diagnostic, global_type) } diff --git a/compiler/checker/comptime.odin b/compiler/checker/comptime.odin index a20a6ad..fa6b480 100644 --- a/compiler/checker/comptime.odin +++ b/compiler/checker/comptime.odin @@ -624,9 +624,15 @@ ct_coerce_value :: proc(state: ^Ct_State, id: Ct_Value_Id, expected: types.Type, return ct_add_value(state, value), true } if value.kind == .Function { - _, _, actual_function, actual_ok := types.function_pointer(value.type, store) + actual_item, actual_ok := types.node(store, value.type) _, _, expected_function, expected_ok := types.function_pointer(expected, store) - if actual_ok && expected_ok && types.equal(actual_function, expected_function) { + 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) { value.type = expected return ct_add_value(state, value), true } @@ -928,7 +934,8 @@ ct_materialize_value :: proc( } return invalid_hir_expr(checker, span, state.diagnostic, value.type) case .Function: - return build_function_value(checker, ast.Function_Id(u32(value.index)), span, expected) + value_expected := expected if types.is_valid(expected) else value.type + return build_function_value(checker, ast.Function_Id(u32(value.index)), span, value_expected) case .None: return add_hir_expr(checker, hir.Expr{ kind=.None, span=span, type=value.type, @@ -1076,16 +1083,20 @@ ct_eval_expr :: proc( if global == ast.INVALID_GLOBAL || int(global) >= len(checker.ast_module.globals) { template := find_template(checker, expr.name, target_pkg, expr_lookup_file(expr, state.file)) if template != ast.INVALID_FUNCTION { - pointer_type, _, function_ok := function_pointer_type_for_template( + function_type, _, function_ok := function_type_for_template( checker, template, state.demanded, state.demanded != nil, ) if function_ok { - return ct_add_value(state, Ct_Value{ - kind=.Function, type=pointer_type, index=u64(template), - }), ct_flow(.Normal), true + id := ct_add_value(state, Ct_Value{ + kind=.Function, type=function_type, index=u64(template), + }) + if types.is_valid(expected) { + return ct_coerce_expr_value(state, id, expected, expr.span) + } + return id, ct_flow(.Normal), true } return INVALID_CT_VALUE, ct_flow(.Normal), ct_failf(state, .Not_Comptime, expr.span, "function '%s' is not comptime-callable as a value", symbol_text(checker, expr.name)) } @@ -1099,7 +1110,7 @@ ct_eval_expr :: proc( return ct_eval_expr(state, g.expr, global_expected, depth+1) case .Function_Literal: template := ast.Function_Id(u32(expr.integer)) - pointer_type, _, ok := function_pointer_type_for_template( + function_type, _, ok := function_type_for_template( checker, template, state.demanded, @@ -1110,9 +1121,13 @@ ct_eval_expr :: proc( state, .Not_Comptime, expr.span, "function literal is not comptime-callable as a value", ) } - return ct_add_value(state, Ct_Value{ - kind=.Function, type=pointer_type, index=u64(template), - }), ct_flow(.Normal), true + id := ct_add_value(state, Ct_Value{ + kind=.Function, type=function_type, index=u64(template), + }) + if types.is_valid(expected) { + return ct_coerce_expr_value(state, id, expected, expr.span) + } + return id, ct_flow(.Normal), true case .Comptime: if expr.left != ast.INVALID_EXPR { return ct_eval_expr(state, expr.left, expected, depth+1) @@ -4164,3 +4179,27 @@ build_comptime_expr :: proc( } return invalid_hir_expr(checker, expr.span, diagnostic, expected) } + +try_build_specialization_expr :: proc( + checker: ^Checker, + expr_id: ast.Expr_Id, + expected: types.Type, + pkg: ast.Package_Id, + file: ast.File_Id, +) -> (hir.Expr_Id, bool) { + if len(checker.current_comptime_values) == 0 && len(checker.static_bindings) == 0 || + expr_id == ast.INVALID_EXPR || int(expr_id) >= len(checker.ast_module.exprs) { + return hir.INVALID_EXPR, false + } + state := ct_state_make(checker, pkg, file, values=checker.current_comptime_values, diagnose=false) + defer ct_state_destroy(&state) + value, flow, ok := ct_eval_expr(&state, expr_id, expected, 0) + if !ok || flow.kind != .Normal || value == INVALID_CT_VALUE || int(value) >= len(state.values) { + return hir.INVALID_EXPR, false + } + static := state.values[value] + if static.kind != .Function { + return hir.INVALID_EXPR, false + } + return ct_materialize_value(&state, value, checker.ast_module.exprs[expr_id].span, expected), true +} diff --git a/compiler/types/types.odin b/compiler/types/types.odin index 9f7bed2..0ee29f1 100644 --- a/compiler/types/types.odin +++ b/compiler/types/types.odin @@ -957,6 +957,33 @@ is_optional_pointer :: proc(value: Type, store: ^Store) -> bool { return ok && item.kind == .Optional && is_pointer(item.child, store) } +is_comptime_only :: proc(value: Type, store: ^Store, depth := 0) -> bool { + if depth > 256 { + return false + } + item, ok := node(store, value) + if !ok { + return false + } + if item.kind == .Function { + return true + } + if item.kind == .Pointer || item.kind == .Slice || item.kind == .Range || item.kind == .Fallible { + return false + } + if item.kind == .Array || item.kind == .Optional || item.kind == .Distinct || item.kind == .Enum || item.kind == .Alias { + return is_comptime_only(item.child, store, depth+1) + } + if item.kind == .Struct || item.kind == .Union { + for field in fields_for(store, value) { + if is_comptime_only(field.type, store, depth+1) { + return true + } + } + } + return false +} + is_runtime_value :: proc(value: Type, store: ^Store, depth := 0) -> bool { if depth > 256 { return false @@ -981,7 +1008,8 @@ is_runtime_value :: proc(value: Type, store: ^Store, depth := 0) -> bool { } if value_kind == .Struct || value_kind == .Union { item, ok := node(store, value) - return ok && item.declared && !item.opaque && (!item.c_layout || item.field_count > 0) + return ok && item.declared && !item.opaque && (!item.c_layout || item.field_count > 0) && + !is_comptime_only(value, store) } if value_kind == .Fallible { item, ok := node(store, value) @@ -1265,6 +1293,20 @@ function_pointer :: proc(value: Type, store: ^Store) -> (pointer_item, function_ return pointer_node, function_node, pointer_node.child, true } +callable_function :: proc(value: Type, store: ^Store) -> (pointer_item, function_item: Node, function_type: Type, ok: bool) { + item, item_ok := node(store, value) + if item_ok && item.kind == .Function { + return {}, item, value, true + } + return function_pointer(value, store) +} + +can_coerce_function_pointer :: proc(actual, expected: Type, store: ^Store) -> bool { + actual_item, actual_ok := node(store, actual) + _, _, expected_function, expected_ok := function_pointer(expected, store) + return actual_ok && actual_item.kind == .Function && expected_ok && equal(actual, expected_function) +} + replace_pointer_child :: proc(store: ^Store, value, child: Type) -> (Type, bool) { item, ok := node(store, value) if !ok { diff --git a/compiler_tests.odin b/compiler_tests.odin index d2154b4..eb81a0c 100644 --- a/compiler_tests.odin +++ b/compiler_tests.odin @@ -2829,17 +2829,21 @@ main func() void { @(test) comptime_function_parameters_specialize_and_lower_directly :: proc(t: ^testing.T) { - text := `Callback_Config :: struct { call @func(value i32) i32 } + text := `Callback_Config :: struct { call func(value i32) i32 } +Callback_Choice :: union(enum) { + call func(value i32) i32 + empty void +} increment func(value i32) i32 { return value + 1 } decrement func(value i32) i32 { return value - 1 } external c_func(value i32) i32 -apply func($callback @func(value i32) i32, value i32) i32 { +apply func($callback func(value i32) i32, value i32) i32 { return callback(value) } -apply_c func($callback *c_func(value i32) i32, value i32) i32 { +apply_c func($callback c_func(value i32) i32, value i32) i32 { return callback(value) } @@ -2847,6 +2851,33 @@ apply_config func($config Callback_Config, value i32) i32 { return config.call(value) } +apply_array func($callbacks [2]func(value i32) i32, value i32) i32 { + return callbacks[0](value) + callbacks[1](value) +} + +apply_optional func($callback ?func(value i32) i32, value i32) i32 { + return callback?(value) +} + +apply_choice func($choice Callback_Choice, value i32) i32 { + return match choice { + .call |callback|: callback(value) + .empty: value + } +} + +apply_pointer func($callback @func(value i32) i32, value i32) i32 { + return callback(value) +} + +call_pointer func(callback @func(value i32) i32, value i32) i32 { + return callback(value) +} + +materialize func($callback func(value i32) i32, value i32) i32 { + return call_pointer(callback, value) +} + main func() i32 { a i32 :: apply(increment, 1) b i32 :: apply(increment, 2) @@ -2854,7 +2885,12 @@ main func() i32 { d i32 :: apply(func(value i32) i32 { return value + 2 }, 4) e i32 :: apply_c(external, 5) f i32 :: apply_config(Callback_Config {call = increment}, 6) - return a + b + c + d + e + f + g i32 :: apply_array([increment, decrement], 7) + h i32 :: apply_optional(increment, 8) + i i32 :: apply_choice(Callback_Choice {call = increment}, 9) + j i32 :: apply_pointer(increment, 10) + k i32 :: materialize(increment, 11) + return a + b + c + d + e + f + g + h + i + j + k } ` stable_names: [dynamic]string @@ -2875,11 +2911,21 @@ main func() i32 { apply_count := 0 callback_specializations := 0 + found_materialization := false for function in ir_module.functions { plain_apply := strings.contains(function.link_name, "bro__p0__apply__") callback_specialization := plain_apply || strings.contains(function.link_name, "bro__p0__apply_c__") || - strings.contains(function.link_name, "bro__p0__apply_config__") + strings.contains(function.link_name, "bro__p0__apply_config__") || + strings.contains(function.link_name, "bro__p0__apply_array__") || + strings.contains(function.link_name, "bro__p0__apply_optional__") || + strings.contains(function.link_name, "bro__p0__apply_choice__") || + strings.contains(function.link_name, "bro__p0__apply_pointer__") + if strings.contains(function.link_name, "bro__p0__materialize__") { + for instruction in function.instructions { + found_materialization = found_materialization || instruction.op == .Function_Address + } + } if !callback_specialization { continue } @@ -2906,7 +2952,8 @@ main func() i32 { testing.expect_value(t, len(diagnostics.items), 0) testing.expect_value(t, apply_count, 3) - testing.expect_value(t, callback_specializations, 5) + testing.expect_value(t, callback_specializations, 9) + testing.expect(t, found_materialization) ir.destroy_module(&ir_module) hir.destroy_module(&hir_module) @@ -2917,6 +2964,110 @@ main func() i32 { } } +@(test) +comptime_only_function_identities_reject_runtime_storage_and_abi_use :: proc(t: ^testing.T) { + text := `Callback :: alias func(value i32) i32 +Config :: struct { callback Callback } +Bad_C :: c_struct { callback c_func(value i32) i32 } + +increment func(value i32) i32 { return value + 1 } +bad_param func(callback Callback) i32 { return callback(1) } +bad_config func(config Config) i32 { return config.callback(1) } +bad_result func() Callback { return increment } +stored Callback = increment + +main func() void { + local Callback = increment + _ = bad_result() +} +` + source_file := source.Source{path="test.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) + + found_param := false + found_config := false + found_result := false + found_global := false + found_local := false + found_c_field := false + for diagnostic in diagnostics.items { + found_param = found_param || strings.contains(diagnostic.message, "parameter 'callback' has a comptime-only type") + found_config = found_config || strings.contains(diagnostic.message, "parameter 'config' has a comptime-only type") + found_result = found_result || strings.contains(diagnostic.message, "has a comptime-only result") + found_global = found_global || strings.contains(diagnostic.message, "global 'stored' has a comptime-only type") + found_local = found_local || strings.contains(diagnostic.message, "local 'local' has a comptime-only type") + found_c_field = found_c_field || strings.contains(diagnostic.message, "c_struct fields must have C-layout-compatible types") + } + testing.expect(t, found_param) + testing.expect(t, found_config) + testing.expect(t, found_result) + testing.expect(t, found_global) + testing.expect(t, found_local) + testing.expect(t, found_c_field) +} + +@(test) +function_pointers_do_not_coerce_back_to_bare_identities :: proc(t: ^testing.T) { + text := `Callback :: alias func(value i32) i32 +increment func(value i32) i32 { return value + 1 } +apply func($callback Callback, value i32) i32 { return callback(value) } +pointer @func(value i32) i32 :: increment +main func() i32 { return apply(pointer, 1) } +` + source_file := source.Source{path="test.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) + + found := false + for diagnostic in diagnostics.items { + found = found || strings.contains(diagnostic.message, "comptime argument") + } + testing.expect(t, found) +} + +@(test) +bodyless_c_function_identity_is_not_comptime_executable :: proc(t: ^testing.T) { + text := `external c_func(value i32) i32 +apply_c func($callback c_func(value i32) i32, value i32) i32 { return callback(value) } +answer :: $apply_c(external, 1) +main func() i32 { return answer } +` + source_file := source.Source{path="test.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) + + found := false + for diagnostic in diagnostics.items { + found = found || strings.contains(diagnostic.message, "runtime-only") + } + testing.expect(t, found) +} + @(test) milestone_37_expand_loop_control_must_be_statically_resolvable :: proc(t: ^testing.T) { text := `main func() void { @@ -4732,7 +4883,7 @@ main func() void { found := false for diagnostic in diagnostics.items { - found = found || strings.contains(diagnostic.message, "call target is not a function pointer") + found = found || strings.contains(diagnostic.message, "call target is not callable") } testing.expect(t, found) } @@ -8591,10 +8742,12 @@ Point :: alias facade.RenamedPoint counter :: alias facade.counter answer :: alias facade.answer ` - app_text := `dep :: import "../dep" +app_text := `dep :: import "../dep" facade :: import "../facade" top :: import "../top" +apply func($callback func() i32) i32 { return callback() } + main func() i32 { box top.Box(i32) :: top.Box(i32) { value = 2 } point top.Point :: top.Point { value = 3 } @@ -8603,6 +8756,7 @@ main func() i32 { top.counter = 7 if box.value != 2 or point.value != 3 { return 1 } if scalar != 5 or top.answer() != 40 or facade.local_answer() != 40 or dep.counter != 7 { return 2 } + if apply(top.answer) != 40 or apply(facade.answer) != 40 { return 3 } _ = maybe return 0 } diff --git a/examples/programs/comptime_v1/main.bro b/examples/programs/comptime_v1/main.bro index f784e07..89e27d3 100644 --- a/examples/programs/comptime_v1/main.bro +++ b/examples/programs/comptime_v1/main.bro @@ -10,7 +10,12 @@ Point :: struct { } Callback_Config :: struct { - call @func(value i32) i32 + call func(value i32) i32 +} + +Callback_Choice :: union(enum) { + call func(value i32) i32 + empty void } Box :: union(enum) { @@ -90,11 +95,11 @@ decrement_c c_func(value i32) i32 { return value - 1 } -apply_comptime func($callback @func(value i32) i32, value i32) i32 { +apply_comptime func($callback func(value i32) i32, value i32) i32 { return callback(value) } -apply_comptime_c func($callback *c_func(value i32) i32, value i32) i32 { +apply_comptime_c func($callback c_func(value i32) i32, value i32) i32 { return callback(value) } @@ -102,10 +107,29 @@ apply_comptime_config func($config Callback_Config, value i32) i32 { return config.call(value) } +apply_comptime_array func($callbacks [2]func(value i32) i32, value i32) i32 { + return callbacks[0](value) + callbacks[1](value) +} + +apply_comptime_optional func($callback ?func(value i32) i32, value i32) i32 { + return callback?(value) +} + +apply_comptime_choice func($choice Callback_Choice, value i32) i32 { + return match choice { + .call |callback|: callback(value) + .empty: value + } +} + call_native func(callback @func(value i32) i32, value i32) i32 { return callback(value) } +materialize_callback func($callback func(value i32) i32, value i32) i32 { + return call_native(callback, value) +} + call_fallible func(callback @func(flag bool) i32 ! Error, flag bool) i32 ! Error { return try callback(flag) } @@ -208,6 +232,9 @@ main func() i32 { comptime_literal i32 :: $apply_comptime(func(value i32) i32 { return value + 2 }, 12) comptime_c_callback i32 :: $apply_comptime_c(decrement_c, 15) comptime_config i32 :: $apply_comptime_config(Callback_Config {call = increment}, 15) + comptime_array i32 :: $apply_comptime_array([increment, double], 3) + comptime_optional i32 :: $apply_comptime_optional(increment, 16) + comptime_choice i32 :: $apply_comptime_choice(Callback_Choice {call = double}, 9) fallible_ok i32 :: $call_fallible(may_fail, false) catch 0 fallible_err i32 :: $call_fallible(may_fail, true) catch |e| { result i32 :: match e { @@ -268,7 +295,8 @@ main func() i32 { return 17 } if comptime_callback != 13 or comptime_literal != 14 or - comptime_c_callback != 14 or comptime_config != 16 { + comptime_c_callback != 14 or comptime_config != 16 or + comptime_array != 10 or comptime_optional != 17 or comptime_choice != 18 { return 18 } if apply_comptime(increment, 20) != 21 or apply_comptime(increment, 21) != 22 or @@ -282,5 +310,13 @@ main func() i32 { apply_comptime_config(Callback_Config {call = increment}, 20) != 21 { return 21 } + if apply_comptime_array([increment, double], 4) != 13 or + apply_comptime_optional(increment, 4) != 5 or + apply_comptime_choice(Callback_Choice {call = double}, 4) != 8 { + return 22 + } + if materialize_callback(increment, 30) != 31 { + return 23 + } return 0 }