diff --git a/LANGUAGE.md b/LANGUAGE.md index f826e0e..6f3095a 100644 --- a/LANGUAGE.md +++ b/LANGUAGE.md @@ -52,6 +52,7 @@ roadmap and milestone history. ### functions, C interop, and linking - 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 - 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 - Apple Silicon C ABI lowering for scalars, pointers, fixed-signature plain records/unions, small aggregates, homogeneous float aggregates, and indirect aggregate returns @@ -79,7 +80,7 @@ roadmap and milestone history. ## PLANNED / DEFERRED -- comptime polymorphism +- comptime type parameters and comptime-evaluable functions - tuples and native Brolang variadic functions - exporting Brolang functions to C and broader target-specific C ABI lowering - non-plain C record layouts such as bitfields, packed records, flexible arrays, qualified fields, and C variadic record arguments diff --git a/README.md b/README.md index 9525a6c..2980c2f 100644 --- a/README.md +++ b/README.md @@ -136,6 +136,7 @@ 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 +- Integer comptime value parameters (`func($N usize) [N]u8`) specialized by value - Bodyless concrete C function declarations with exact external symbol names - Bodyless manual and imported C variadic declarations with default argument promotions - Ordered linking of additional C sources, objects, archives, and libraries diff --git a/TODO.md b/TODO.md index 0234873..d981dcf 100644 --- a/TODO.md +++ b/TODO.md @@ -630,11 +630,23 @@ - imports beginning with `@` resolve from the compiler process cwd / project root - `heap :: import "@std/mem/heap"` works from any package depth without `../../../` path math -27. comptime polymorphism (zig inspired) +27. comptime integer value parameters (implemented; v1) + - `$N` marks an integer comptime parameter in a normal `func` signature: + `make_array func($N usize) [N]u8` + - callers pass a compile-time integer expression; the value specializes the function + and is omitted from the runtime ABI + - inside the specialization, `N` is visible as an immutable compile-time integer in + array counts, types, and body expressions + - v1 intentionally supports integer values only; no comptime branch pruning or + user-function execution -28. comptime execution +27.5 comptime type parameters (deferred) + - planned shape: `max func($T type, a, b T) T` -29. brolang build system (requires comptime execution) +27.6 comptime-evaluable constants/functions (deferred) + - planned shape: `$x :: 32` and `$sum func(a, b int) int { ... }` + +28. brolang build system (requires comptime execution) ## A word on multi-unwrap @@ -1221,6 +1233,15 @@ data :: read_file(path) catch |e| match e { | `yield :label value` | Provide value from labeled block | | `return` / `return value` | Exit the current function; in fallible functions, return value dispatches by type | +## A word on comptime + +``` +make_array func($N usize) [N]u8 { ... } # implemented: integer comptime value params +max func($T type, a, b T) T { ... } # deferred: comptime type params +$x :: 32 # deferred: comptime evaluable constant +$sum func(a, b int) int { ... } # deferred: comptime evaluable function +``` + ## A word on memory allocation (NOTE THAT SYNTAX MAY NOT MATCH BROLANG EXACTLY AND SHOULD BE TAKEN WITH A GRAIN OF SALT - INSPIRATION ONLY) diff --git a/compiler/ast/ast.odin b/compiler/ast/ast.odin index b166f1a..e7c3078 100644 --- a/compiler/ast/ast.odin +++ b/compiler/ast/ast.odin @@ -123,6 +123,7 @@ Param :: struct { name: symbol.Id, span: source.Span, type: Type_Syntax, + comptime_value: bool, } Stmt_Kind :: enum u8 { diff --git a/compiler/checker/checker.odin b/compiler/checker/checker.odin index a17525f..144c07b 100644 --- a/compiler/checker/checker.odin +++ b/compiler/checker/checker.odin @@ -28,10 +28,17 @@ spec_index :: proc(id: Spec_Id, count: int) -> (int, bool) { Spec :: struct { template: ast.Function_Id, args: []types.Type, + comptime_values: []Comptime_Value, result: types.Type, hir_id: hir.Function_Id, } +Comptime_Value :: struct { + name: symbol.Id, + type: types.Type, + value: i128, +} + Infer_Local :: struct { name: symbol.Id, type: types.Type, @@ -169,6 +176,7 @@ Checker :: struct { sink_symbol: symbol.Id, current_result: types.Type, current_build_ctx: ^Build_Ctx, + current_comptime_values: []Comptime_Value, target: target.Target, allocator: mem.Allocator, } @@ -187,6 +195,61 @@ type_label :: proc(checker: ^Checker, value: types.Type) -> string { return types.name(value) } +find_comptime_value :: proc(values: []Comptime_Value, name: symbol.Id) -> (Comptime_Value, bool) { + for index := len(values) - 1; index >= 0; index -= 1 { + if values[index].name == name { + return values[index], true + } + } + return {}, false +} + +current_comptime_value :: proc(checker: ^Checker, name: symbol.Id) -> (Comptime_Value, bool) { + return find_comptime_value(checker.current_comptime_values, name) +} + +function_has_comptime_params :: proc(function: ast.Function) -> bool { + for param in function.params { + if param.comptime_value { + return true + } + } + return false +} + +runtime_param_count :: proc(function: ast.Function) -> int { + count := 0 + for param in function.params { + if !param.comptime_value { + count += 1 + } + } + return count +} + +comptime_param_count :: proc(function: ast.Function) -> int { + count := 0 + for param in function.params { + if param.comptime_value { + count += 1 + } + } + return count +} + +comptime_values_equal :: proc(left, right: []Comptime_Value) -> bool { + if len(left) != len(right) { + return false + } + for value, index in left { + other := right[index] + if value.name != other.name || !types.equal(value.type, other.type) || value.value != other.value { + return false + } + } + return true +} + Constant_Frame :: struct { expr: ast.Expr_Id, stage: u8, @@ -311,6 +374,11 @@ eval_integer_constant_in_context :: proc( case .Integer: return Constant{kind = .Value, value = i128(expr.integer)} case .Name: + if !symbol.is_valid(expr.qualifier) { + if value, ok := current_comptime_value(checker, expr.name); ok { + return Constant{kind = .Value, value = value.value} + } + } target_pkg, available := expr_package(checker, expr, pkg, file, false) if !available { return Constant{kind = .Not_Constant} @@ -767,7 +835,8 @@ function_signatures_equal :: proc(left, right: ast.Function) -> bool { return false } for param, index in left.params { - if param.type != right.params[index].type { + if param.type != right.params[index].type || + param.comptime_value != right.params[index].comptime_value { return false } } @@ -792,6 +861,79 @@ call_arg_expected :: proc(checker: ^Checker, function: ast.Function, index: int) return declared } +collect_comptime_values :: proc( + checker: ^Checker, + function: ast.Function, + args: []ast.Expr_Id, + pkg: ast.Package_Id, + file: ast.File_Id, + diagnose := false, +) -> ([]Comptime_Value, bool) { + if !function_has_comptime_params(function) { + return nil, true + } + values: [dynamic]Comptime_Value + values.allocator = checker.allocator + ok := true + for param, index in function.params { + if !param.comptime_value { + continue + } + span := param.span + if index < len(args) && args[index] != ast.INVALID_EXPR && int(args[index]) < len(checker.ast_module.exprs) { + span = checker.ast_module.exprs[args[index]].span + } + declared := type_from_syntax(checker, param.type, function.pkg, function.file) + if !types.is_concrete_integer(declared) { + if diagnose { + source.addf( + checker.diagnostics, + param.span, + "comptime parameter '%s' requires a concrete integer type", + symbol_text(checker, param.name), + ) + } + ok = false + continue + } + constant := Constant{kind = .Not_Constant} + if index < len(args) { + constant = eval_integer_constant_in_context(checker, args[index], pkg, file) + } + if constant.kind != .Value { + if diagnose { + source.addf( + checker.diagnostics, + span, + "argument for comptime parameter '%s' must be a compile-time integer expression", + symbol_text(checker, param.name), + ) + } + ok = false + continue + } + if !fits_integer_type(constant.value, declared, checker.target) { + if diagnose { + source.addf( + checker.diagnostics, + span, + "integer constant %d does not fit in %s", + constant.value, + types.name(declared), + ) + } + ok = false + continue + } + append(&values, Comptime_Value{name=param.name, type=declared, value=constant.value}) + } + if !ok { + delete(values) + return nil, false + } + return values[:], true +} + callable_arg_expected :: proc(function_type: types.Type, function_item: types.Node, store: ^types.Store, index: int) -> types.Type { if index < 0 || index >= int(function_item.field_count) { return types.INVALID @@ -818,6 +960,9 @@ function_value_signature :: proc( if !function.c_abi || types.is_valid(function.error) { return nil, types.INVALID, false } + if function_has_comptime_params(function) { + return nil, types.INVALID, false + } result = type_from_syntax(checker, function.result, function.pkg, function.file) if !types.is_void(result) && !is_runtime_type(checker, result) { return nil, types.INVALID, false @@ -1027,13 +1172,33 @@ validate_declarations :: proc(checker: ^Checker) { if len(function.unsupported_reason) > 0 { continue } + has_comptime := function_has_comptime_params(function) locals: [dynamic]symbol.Id locals.allocator = checker.allocator for param in function.params { - if diagnostic := add_unsupported_type_diagnostic(checker, param.span, type_from_syntax(checker, param.type, function.pkg, function.file)); - diagnostic != source.INVALID_DIAGNOSTIC { - checker.template_diagnostics[function_id] = diagnostic - continue + param_type := type_from_syntax(checker, param.type, function.pkg, function.file) + if param.comptime_value { + if function.c_abi { + checker.template_diagnostics[function_id] = source.add( + checker.diagnostics, + param.span, + "comptime parameters require 'func', not 'c_func'", + ) + } + if !types.is_concrete_integer(param_type) { + checker.template_diagnostics[function_id] = source.addf( + checker.diagnostics, + param.span, + "comptime parameter '%s' requires a concrete integer type", + symbol_text(checker, param.name), + ) + } + } else if !has_comptime { + if diagnostic := add_unsupported_type_diagnostic(checker, param.span, param_type); + diagnostic != source.INVALID_DIAGNOSTIC { + checker.template_diagnostics[function_id] = diagnostic + continue + } } if param.type == types.VOID { source.add( @@ -1051,7 +1216,7 @@ validate_declarations :: proc(checker: ^Checker) { ) } append(&locals, param.name) - if types.contains_c_struct_by_value(type_from_syntax(checker, param.type, function.pkg, function.file), &checker.module.types) { + if !has_comptime && types.contains_c_struct_by_value(param_type, &checker.module.types) { checker.template_diagnostics[function_id] = source.addf( checker.diagnostics, param.span, @@ -1060,17 +1225,20 @@ validate_declarations :: proc(checker: ^Checker) { ) } } - if diagnostic := add_unsupported_type_diagnostic(checker, function.span, type_from_syntax(checker, function.result, function.pkg, function.file)); - diagnostic != source.INVALID_DIAGNOSTIC { - checker.template_diagnostics[function_id] = diagnostic - } - if types.contains_c_struct_by_value(type_from_syntax(checker, function.result, function.pkg, function.file), &checker.module.types) { - checker.template_diagnostics[function_id] = source.addf( - checker.diagnostics, - function.span, - "C records cannot be returned by value from '%s'", - symbol_text(checker, function.name), - ) + if !has_comptime { + result_type := type_from_syntax(checker, function.result, function.pkg, function.file) + if diagnostic := add_unsupported_type_diagnostic(checker, function.span, result_type); + diagnostic != source.INVALID_DIAGNOSTIC { + checker.template_diagnostics[function_id] = diagnostic + } + if types.contains_c_struct_by_value(result_type, &checker.module.types) { + checker.template_diagnostics[function_id] = source.addf( + checker.diagnostics, + function.span, + "C records cannot be returned by value from '%s'", + symbol_text(checker, function.name), + ) + } } if types.is_valid(function.error) { error_type := type_from_syntax(checker, function.error, function.pkg, function.file) @@ -1298,23 +1466,38 @@ find_infer_local_index :: proc(locals: []Infer_Local, name: symbol.Id) -> (int, return -1, false } -find_spec :: proc(checker: ^Checker, template: ast.Function_Id, actual_args: []types.Type) -> Spec_Id { +find_spec :: proc( + checker: ^Checker, + template: ast.Function_Id, + actual_args: []types.Type, + comptime_values: []Comptime_Value = nil, +) -> Spec_Id { function := checker.ast_module.functions[template] for spec, index in checker.specs { - if spec.template != template || len(spec.args) != len(function.params) { + if spec.template != template || len(spec.args) != runtime_param_count(function) || + !comptime_values_equal(spec.comptime_values, comptime_values) { continue } matches := true + runtime_index := 0 + previous_comptime := checker.current_comptime_values + checker.current_comptime_values = comptime_values for param, param_index in function.params { + if param.comptime_value { + continue + } actual := types.INVALID if param_index < len(actual_args) { actual = actual_args[param_index] } - if !types.equal(spec.args[param_index], specialized_param_type(checker, param.type, actual, function.pkg, function.file)) { + if runtime_index >= len(spec.args) || + !types.equal(spec.args[runtime_index], specialized_param_type(checker, param.type, actual, function.pkg, function.file)) { matches = false break } + runtime_index += 1 } + checker.current_comptime_values = previous_comptime if matches { return spec_id(index) } @@ -1340,8 +1523,19 @@ specialized_param_type :: proc( return declared } -can_specialize :: proc(checker: ^Checker, function: ast.Function, actual_args: []types.Type) -> bool { +can_specialize :: proc( + checker: ^Checker, + function: ast.Function, + actual_args: []types.Type, + comptime_values: []Comptime_Value = nil, +) -> bool { + previous_comptime := checker.current_comptime_values + checker.current_comptime_values = comptime_values + defer checker.current_comptime_values = previous_comptime for param, index in function.params { + if param.comptime_value { + continue + } actual := types.INVALID if index < len(actual_args) { actual = actual_args[index] @@ -1353,20 +1547,34 @@ can_specialize :: proc(checker: ^Checker, function: ast.Function, actual_args: [ return true } -ensure_spec :: proc(checker: ^Checker, template: ast.Function_Id, actual_args: []types.Type) -> Spec_Id { - if existing := find_spec(checker, template, actual_args); existing != INVALID_SPEC { +ensure_spec :: proc( + checker: ^Checker, + template: ast.Function_Id, + actual_args: []types.Type, + comptime_values: []Comptime_Value = nil, +) -> Spec_Id { + if existing := find_spec(checker, template, actual_args, comptime_values); existing != INVALID_SPEC { return existing } function := checker.ast_module.functions[template] + previous_comptime := checker.current_comptime_values + checker.current_comptime_values = comptime_values + defer checker.current_comptime_values = previous_comptime signature: [dynamic]types.Type signature.allocator = checker.allocator for param, index in function.params { + if param.comptime_value { + continue + } actual := types.INVALID if index < len(actual_args) { actual = actual_args[index] } append(&signature, specialized_param_type(checker, param.type, actual, function.pkg, function.file)) } + comptime_signature: [dynamic]Comptime_Value + comptime_signature.allocator = checker.allocator + append(&comptime_signature, ..comptime_values) result := function_channel_type(checker, function) if function.pkg == 0 && function.name == checker.main_symbol && function.result == types.INT && !types.is_valid(function.error) { @@ -1375,7 +1583,13 @@ ensure_spec :: proc(checker: ^Checker, template: ast.Function_Id, actual_args: [ index := spec_id(len(checker.specs)) append( &checker.specs, - Spec{template = template, args = signature[:], result = result, hir_id = hir.INVALID_FUNCTION}, + Spec{ + template = template, + args = signature[:], + comptime_values = comptime_signature[:], + result = result, + hir_id = hir.INVALID_FUNCTION, + }, ) return index } @@ -1662,6 +1876,13 @@ infer_expr :: proc( } } } + if !types.is_valid(last) { + if !symbol.is_valid(expr.qualifier) { + if value, ok := current_comptime_value(checker, expr.name); ok { + last = value.type + } + } + } if !types.is_valid(last) { if enum_type, enum_ok := enum_type_from_name_expr(checker, expr, pkg, file); enum_ok { _, member_ok := find_enum_member(checker, enum_type, expr.name) @@ -1859,13 +2080,16 @@ infer_expr :: proc( } } } + comptime_values, comptime_ok := collect_comptime_values(checker, function, expr.args, pkg, file) + defer delete(comptime_values, checker.allocator) if valid_call_arity(function, len(expr.args)) && - can_specialize(checker, function, stack[frame_index].args) { + comptime_ok && + can_specialize(checker, function, stack[frame_index].args, comptime_values) { spec := INVALID_SPEC if demanded == nil { - spec = ensure_spec(checker, frame.template, stack[frame_index].args) + spec = ensure_spec(checker, frame.template, stack[frame_index].args, comptime_values) } else { - spec = find_spec(checker, frame.template, stack[frame_index].args) + spec = find_spec(checker, frame.template, stack[frame_index].args, comptime_values) mark_spec_demanded(checker, spec, demanded) } if spec != INVALID_SPEC { @@ -2252,6 +2476,9 @@ infer_spec_locals_and_result :: proc( ) -> ([]types.Type, types.Type) { spec := checker.specs[id] function := checker.ast_module.functions[spec.template] + previous_comptime := checker.current_comptime_values + checker.current_comptime_values = spec.comptime_values + defer checker.current_comptime_values = previous_comptime declared := type_from_syntax(checker, function.result, function.pkg, function.file) if function.pkg == 0 && function.name == checker.main_symbol && function.result == types.INT { declared = types.I32 @@ -2262,12 +2489,17 @@ infer_spec_locals_and_result :: proc( locals.allocator = checker.allocator defer delete(locals) local_types := make([]types.Type, len(checker.ast_module.statements), checker.allocator) + runtime_index := 0 for param, index in function.params { + if param.comptime_value { + continue + } param_type := types.INVALID - if index < len(spec.args) { - param_type = spec.args[index] + if runtime_index < len(spec.args) { + param_type = spec.args[runtime_index] } append(&locals, Infer_Local{name=param.name, type=param_type, declared=param_type, statement=ast.INVALID_STMT}) + runtime_index += 1 } result := types.INVALID @@ -2716,6 +2948,7 @@ prune_specs :: proc(checker: ^Checker) { for spec in checker.specs { if spec.hir_id == hir.INVALID_FUNCTION { delete(spec.args, checker.allocator) + delete(spec.comptime_values, checker.allocator) continue } checker.specs[retained] = spec @@ -4090,6 +4323,22 @@ build_expr :: proc( }) } } + if last == hir.INVALID_EXPR { + if !symbol.is_valid(expr.qualifier) { + if value, ok := current_comptime_value(checker, expr.name); ok { + expected_type := value.type + if types.is_concrete_integer(frame.expected) || types.is_float(frame.expected, checker.target) { + expected_type = frame.expected + } + last = build_constant_expr( + checker, + expr, + Constant{kind=.Value, value=value.value}, + expected_type, + ) + } + } + } if last == hir.INVALID_EXPR { if enum_type, enum_ok := enum_type_from_name_expr(checker, expr, pkg, file); enum_ok { last = enum_member_hir(checker, enum_type, expr.name, expr.span) @@ -4340,14 +4589,22 @@ build_expr :: proc( continue } } + function := checker.ast_module.functions[frame.template] + comptime_values, comptime_ok := collect_comptime_values(checker, function, expr.args, pkg, file, diagnose=true) + defer delete(comptime_values, checker.allocator) arg_violation := source.INVALID_DIAGNOSTIC - { - params := checker.ast_module.functions[frame.template].params + if comptime_ok { + previous_comptime := checker.current_comptime_values + checker.current_comptime_values = comptime_values + params := function.params for index in 0..= len(stack[frame_index].arg_types) { break } - declared := type_from_syntax(checker, params[index].type, checker.ast_module.functions[frame.template].pkg, checker.ast_module.functions[frame.template].file) + if params[index].comptime_value { + continue + } + declared := type_from_syntax(checker, params[index].type, function.pkg, function.file) actual := stack[frame_index].arg_types[index] if types.is_constraint(declared) && types.is_valid(actual) && !types.is_valid(types.constraint_target(declared, actual, &checker.module.types)) { @@ -4360,14 +4617,22 @@ build_expr :: proc( break } } + checker.current_comptime_values = previous_comptime + } + spec := INVALID_SPEC + if comptime_ok { + spec = find_spec(checker, frame.template, stack[frame_index].arg_types, comptime_values) } - spec := find_spec(checker, frame.template, stack[frame_index].arg_types) delete(stack[frame_index].arg_types, checker.allocator) stack[frame_index].arg_types = nil - if arg_violation != source.INVALID_DIAGNOSTIC { + if !comptime_ok || arg_violation != source.INVALID_DIAGNOSTIC { delete(stack[frame_index].built_args, checker.allocator) stack[frame_index].built_args = nil - last = invalid_hir_expr(checker, expr.span, arg_violation) + diagnostic := arg_violation + if diagnostic == source.INVALID_DIAGNOSTIC { + diagnostic = source.add(checker.diagnostics, expr.span, "invalid comptime argument") + } + last = invalid_hir_expr(checker, expr.span, diagnostic) _ = pop(&stack) continue } @@ -4384,23 +4649,35 @@ build_expr :: proc( _ = pop(&stack) continue } - fixed_count := len(checker.ast_module.functions[frame.template].params) - for index in 0.. string { strings.write_string(&builder, types.name(arg)) } } + for value in spec.comptime_values { + strings.write_string(&builder, "__cv") + if value.value < 0 { + strings.write_string(&builder, "n") + fmt.sbprintf(&builder, "%d", -value.value) + } else { + fmt.sbprintf(&builder, "%d", value.value) + } + } return fmt.aprintf("%s", strings.to_string(builder), allocator = checker.allocator) } @@ -4860,6 +5144,21 @@ build_block :: proc( } continue } + if _, is_comptime := current_comptime_value(checker, statement.name); is_comptime { + id := source.addf( + checker.diagnostics, + statement.span, + "cannot assign comptime parameter '%s'", + 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 + } local, found := find_build_local(ctx.locals^[:], statement.name) if !found { id := source.addf(checker.diagnostics, statement.span, "cannot assign unresolved local '%s'", symbol_text(checker, statement.name)) @@ -6901,6 +7200,9 @@ all_paths_exit :: proc(module: ^hir.Module, stmts: []hir.Stmt_Id) -> bool { build_function :: proc(checker: ^Checker, id: Spec_Id) { spec := checker.specs[id] function := checker.ast_module.functions[spec.template] + previous_comptime := checker.current_comptime_values + checker.current_comptime_values = spec.comptime_values + defer checker.current_comptime_values = previous_comptime signature_diagnostic := source.INVALID_DIAGNOSTIC if !types.is_void(spec.result) && !is_runtime_type(checker, spec.result) { checker.specs[id].result = types.I64 @@ -6945,15 +7247,20 @@ build_function :: proc(checker: ^Checker, id: Spec_Id) { delete(demanded) } - for param, index in function.params { + runtime_index := 0 + for param in function.params { + if param.comptime_value { + continue + } local_id := hir.local_id(len(hir_locals)) param_type := types.INVALID - if index < len(spec.args) { - param_type = spec.args[index] + if runtime_index < len(spec.args) { + param_type = spec.args[runtime_index] } append(&hir_locals, hir.Local{name = param.name, type = param_type, parameter = true}) append(&locals, Build_Local{name = param.name, type = param_type, id = local_id}) append(¶ms, local_id) + runtime_index += 1 } problematic := signature_diagnostic != source.INVALID_DIAGNOSTIC || @@ -7526,6 +7833,7 @@ check :: proc( defer { for spec in checker.specs { delete(spec.args, allocator) + delete(spec.comptime_values, allocator) } delete(checker.specs) delete(checker.function_index, allocator) diff --git a/compiler/lexer/lexer.odin b/compiler/lexer/lexer.odin index a4c8362..b4f8378 100644 --- a/compiler/lexer/lexer.odin +++ b/compiler/lexer/lexer.odin @@ -201,6 +201,9 @@ lex :: proc( case '@': append_token(&stream, source_file, .At, cursor, cursor+1) cursor += 1 + case '$': + append_token(&stream, source_file, .Dollar, cursor, cursor+1) + cursor += 1 case '*': start := cursor cursor += 1 diff --git a/compiler/parser/parser.odin b/compiler/parser/parser.odin index 6292c0e..f101e07 100644 --- a/compiler/parser/parser.odin +++ b/compiler/parser/parser.odin @@ -1577,6 +1577,7 @@ parse_params :: proc(parser: ^Parser) -> ([]ast.Param, bool) { } continue } + _, comptime_value := allow(parser, .Dollar) names: [dynamic]token.Token names.allocator = parser.module.allocator for { @@ -1596,7 +1597,12 @@ parse_params :: proc(parser: ^Parser) -> ([]ast.Param, bool) { } type_syntax := parse_type(parser) for name in names { - append(¶ms, ast.Param{name=name.symbol, span=name.span, type=type_syntax}) + append(¶ms, ast.Param{ + name=name.symbol, + span=name.span, + type=type_syntax, + comptime_value=comptime_value, + }) } delete(names) skip_newlines(parser) diff --git a/compiler/token/token.odin b/compiler/token/token.odin index 78b73d5..dcbcb20 100644 --- a/compiler/token/token.odin +++ b/compiler/token/token.odin @@ -36,6 +36,7 @@ Kind :: enum u8 { Range_Inclusive, Ellipsis, At, + Dollar, Star, Ampersand, Caret, diff --git a/compiler_tests.odin b/compiler_tests.odin index ba7feec..d16086a 100644 --- a/compiler_tests.odin +++ b/compiler_tests.odin @@ -175,6 +175,35 @@ main func() void {} testing.expect_value(t, len(module.functions[0].params), 2) } +@(test) +parser_accepts_comptime_value_params :: proc(t: ^testing.T) { + text := `make func($N usize, value i32) i32 { + return value +} +main func() void {} +` + 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) + module := parser.parse(&stream, &source_file, &diagnostics) + defer ast.destroy_module(&module) + + found_dollar := false + for tok in stream.items { + found_dollar = found_dollar || tok.kind == token.Kind.Dollar + } + + testing.expect_value(t, len(diagnostics.items), 0) + testing.expect(t, found_dollar) + testing.expect_value(t, len(module.functions[0].params), 2) + testing.expect(t, module.functions[0].params[0].comptime_value) + testing.expect(t, !module.functions[0].params[1].comptime_value) +} + @(test) parser_accepts_sentinel_many_item_pointer_types :: proc(t: ^testing.T) { text := `zero func(value [*;0]u8) void {} @@ -1945,6 +1974,116 @@ main func() i32 { } } +@(test) +comptime_value_params_specialize_by_value_and_omit_runtime_args :: proc(t: ^testing.T) { + text := `make_array func($N usize) [N]u8 { + data [N]u8 = undefined + return data +} +main func() void { + four [4]u8 :: make_array(4) + eight [8]u8 :: make_array(8) + again [4]u8 :: make_array(4) + _ = four + _ = eight + _ = again +} +` + 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) + ir_module := lower.lower(&hir_module) + defer ir.destroy_module(&ir_module) + llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols) + defer delete(llvm_text) + + make_specs := 0 + for function in hir_module.functions { + if symbol.resolve(&symbols, function.name) == "make_array" { + make_specs += 1 + testing.expect_value(t, len(function.params), 0) + } + } + + testing.expect_value(t, len(diagnostics.items), 0) + testing.expect_value(t, make_specs, 2) + testing.expect(t, strings.contains(llvm_text, "@bro__p0__make_array__cv4")) + testing.expect(t, strings.contains(llvm_text, "@bro__p0__make_array__cv8")) +} + +@(test) +comptime_value_params_diagnose_invalid_uses :: proc(t: ^testing.T) { + text := `make func($N usize) i32 { + return N +} +tiny func($N u8) i32 { + return N +} +bad_type func($T bool) void {} +bad_use func($N usize) void { + N = 1 + _ = &N +} +main func() void { + x usize = 4 + _ = make(x) + _ = make() + _ = make(1, 2) + _ = make(-1) + _ = tiny(300) + bad_use(4) +} +` + 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_runtime_arg := false + found_missing := false + found_extra := false + found_negative := false + found_range := false + found_bad_type := false + found_assignment := false + found_address := false + for diagnostic in diagnostics.items { + message := diagnostic.message + found_runtime_arg = found_runtime_arg || strings.contains(message, "must be a compile-time integer expression") + found_missing = found_missing || strings.contains(message, "expects 1 arguments, got 0") + found_extra = found_extra || strings.contains(message, "expects 1 arguments, got 2") + found_negative = found_negative || strings.contains(message, "integer constant -1 does not fit in usize") + found_range = found_range || strings.contains(message, "integer constant 300 does not fit in u8") + found_bad_type = found_bad_type || strings.contains(message, "requires a concrete integer type") + found_assignment = found_assignment || strings.contains(message, "cannot assign comptime parameter 'N'") + found_address = found_address || strings.contains(message, "'&' requires an addressable location") + } + + testing.expect(t, found_runtime_arg) + testing.expect(t, found_missing) + testing.expect(t, found_extra) + testing.expect(t, found_negative) + testing.expect(t, found_range) + testing.expect(t, found_bad_type) + testing.expect(t, found_assignment) + testing.expect(t, found_address) +} + @(test) unused_function_signatures_are_validated_eagerly :: proc(t: ^testing.T) { text := `broken func(value, value i8, nope void) void {} @@ -5305,6 +5444,16 @@ cross_package_generic_specializes_from_folded_argument :: proc(t: ^testing.T) { testing.expect_value(t, state.exit_code, 128) } +@(test) +comptime_value_params_compile_and_run :: proc(t: ^testing.T) { + output := "/tmp/brolang-test-comptime-value-params" + defer _ = os.remove(output) + status := compiler_core.compile_package("examples/programs/comptime_value_params", output) + testing.expect_value(t, status, 0) + state := run_executable(output) + testing.expect_value(t, state.exit_code, 0) +} + @(test) lazy_function_body_marks_import_as_used_without_resolving_it :: proc(t: ^testing.T) { output := "/tmp/brolang-test-package-lazy-import" diff --git a/examples/programs/comptime_value_params/main.bro b/examples/programs/comptime_value_params/main.bro new file mode 100644 index 0000000..a72421f --- /dev/null +++ b/examples/programs/comptime_value_params/main.bro @@ -0,0 +1,32 @@ +make_array func($N usize) [N]u8 { + data [N]u8 = undefined + return data +} + +value func($N usize) usize { + return N +} + +main func() i32 { + four [4]u8 :: make_array(4) + if four.len != 4 { + return 1 + } + if value(4) != 4 { + return 2 + } + + eight [8]u8 :: make_array(8) + if eight.len != 8 { + return 3 + } + if value(8) != 8 { + return 4 + } + + again [4]u8 :: make_array(4) + if again.len != 4 { + return 5 + } + return 0 +}