From 6d25f4db9d8db105af1629e6febb01e98f48e4f0 Mon Sep 17 00:00:00 2001 From: kbkpbot Date: Mon, 25 Aug 2025 21:40:09 +0800 Subject: [PATCH] checker,cgen: add comptime match support --- vlib/v/ast/ast.v | 7 +- vlib/v/checker/comptime.v | 84 ++--- vlib/v/checker/match.v | 300 +++++++++++++++--- .../tests/comptime_match_cond_cannot_mut.out | 14 + .../tests/comptime_match_cond_cannot_mut.vv | 18 ++ .../comptime_match_value_different_type.out | 14 + .../comptime_match_value_different_type.vv | 25 ++ .../comptime_match_value_type_mix_check.out | 14 + .../comptime_match_value_type_mix_check.vv | 25 ++ vlib/v/fmt/fmt.v | 15 +- vlib/v/gen/c/cgen.v | 6 +- vlib/v/gen/c/comptime.v | 277 ++++++---------- vlib/v/parser/expr.v | 5 +- vlib/v/parser/if_match.v | 18 +- vlib/v/parser/parser.v | 9 + .../comptime/comptime_match_assign_test.v | 62 ++++ .../comptime_match_for_field_type_test.v | 47 +++ .../comptime_match_for_field_value_test.v | 27 ++ .../comptime_match_generic_type_test.v | 40 +++ .../comptime/comptime_match_type_2_test.v | 22 ++ .../comptime/comptime_match_type_check_test.v | 53 ++++ .../comptime_match_value_check_test.v | 114 +++++++ 22 files changed, 923 insertions(+), 273 deletions(-) create mode 100644 vlib/v/checker/tests/comptime_match_cond_cannot_mut.out create mode 100644 vlib/v/checker/tests/comptime_match_cond_cannot_mut.vv create mode 100644 vlib/v/checker/tests/comptime_match_value_different_type.out create mode 100644 vlib/v/checker/tests/comptime_match_value_different_type.vv create mode 100644 vlib/v/checker/tests/comptime_match_value_type_mix_check.out create mode 100644 vlib/v/checker/tests/comptime_match_value_type_mix_check.vv create mode 100644 vlib/v/tests/comptime/comptime_match_assign_test.v create mode 100644 vlib/v/tests/comptime/comptime_match_for_field_type_test.v create mode 100644 vlib/v/tests/comptime/comptime_match_for_field_value_test.v create mode 100644 vlib/v/tests/comptime/comptime_match_generic_type_test.v create mode 100644 vlib/v/tests/comptime/comptime_match_type_2_test.v create mode 100644 vlib/v/tests/comptime/comptime_match_type_check_test.v create mode 100644 vlib/v/tests/comptime/comptime_match_value_check_test.v diff --git a/vlib/v/ast/ast.v b/vlib/v/ast/ast.v index 179a5a29ee..1c314f4947 100644 --- a/vlib/v/ast/ast.v +++ b/vlib/v/ast/ast.v @@ -1262,9 +1262,10 @@ pub mut: @[minify] pub struct MatchExpr { pub: - tok_kind token.Kind - pos token.Pos - comments []Comment // comments before the first branch + is_comptime bool + tok_kind token.Kind + pos token.Pos + comments []Comment // comments before the first branch pub mut: cond Expr branches []MatchBranch diff --git a/vlib/v/checker/comptime.v b/vlib/v/checker/comptime.v index d744701fc3..1befa70d85 100644 --- a/vlib/v/checker/comptime.v +++ b/vlib/v/checker/comptime.v @@ -969,6 +969,15 @@ fn (mut c Checker) comptime_if_to_ifdef(name string) !string { return error('bad os ifdef name "${name}"') } +// check if `ident` is a function generic, such as `T` +fn (mut c Checker) is_generic_ident(ident string) bool { + if !isnil(c.table.cur_fn) && ident in c.table.cur_fn.generic_names + && c.table.cur_fn.generic_names.len == c.table.cur_concrete_types.len { + return true + } + return false +} + fn (mut c Checker) get_expr_type(cond ast.Expr) ast.Type { match cond { ast.Ident { @@ -978,6 +987,10 @@ fn (mut c Checker) get_expr_type(cond ast.Expr) ast.Type { || cond.name == c.comptime.comptime_for_field_var) { // struct field return c.type_resolver.get_type_from_comptime_var(cond) + } else if c.is_generic_ident(cond.name) { + // generic type `T` + idx := c.table.cur_fn.generic_names.index(cond.name) + return c.table.cur_concrete_types[idx] } else if var := cond.scope.find_var(cond.name) { // var checked_type = c.unwrap_generic(var.typ) @@ -1042,6 +1055,35 @@ fn (mut c Checker) get_expr_type(cond ast.Expr) ast.Type { } } +fn (mut c Checker) check_compatible_types(left_type ast.Type, left_name string, expr ast.Expr) bool { + if expr is ast.ComptimeType { + return c.type_resolver.is_comptime_type(left_type, expr as ast.ComptimeType) + } else if expr is ast.TypeNode { + typ := c.get_expr_type(expr) + right_type := c.unwrap_generic(typ) + right_sym := c.table.sym(right_type) + if right_sym.kind == .placeholder || right_type.has_flag(.generic) { + c.error('unknown type `${right_sym.name}`', expr.pos) + } + if right_sym.kind == .interface && right_sym.info is ast.Interface { + return left_type.has_flag(.option) == right_type.has_flag(.option) + && c.table.does_type_implement_interface(left_type, right_type) + } + if right_sym.info is ast.FnType && c.comptime.comptime_for_method_var == left_name { + return c.table.fn_signature(right_sym.info.func, + skip_receiver: true + type_only: true + ) == c.table.fn_signature(c.comptime.comptime_for_method, + skip_receiver: true + type_only: true + ) + } else { + return left_type == right_type + } + } + return false +} + // comptime_if_cond evaluate the `cond` and return (`is_true`, `keep_stmts`) // `is_true` is the evaluate result of `cond`; // `keep_stmts` meaning the branch is a `multi pass branch`, we should keep the branch stmts even `is_true` is false, such as `$if T is int {` @@ -1141,45 +1183,9 @@ fn (mut c Checker) comptime_if_cond(mut cond ast.Expr, mut sb strings.Builder) ( } // iter the `type_array`, for `is` and `!is`, it has only one element for expr in type_array { - if expr is ast.ComptimeType { - is_true = c.type_resolver.is_comptime_type(left_type, - expr as ast.ComptimeType) - if is_true { - break - } - } else if expr is ast.TypeNode { - typ := c.get_expr_type(expr) - right_type := c.unwrap_generic(typ) - right_sym := c.table.sym(right_type) - if right_sym.kind == .placeholder || right_type.has_flag(.generic) { - c.error('unknown type `${right_sym.name}`', expr.pos) - } - if right_sym.kind == .interface && right_sym.info is ast.Interface { - is_true = - left_type.has_flag(.option) == right_type.has_flag(.option) - && c.table.does_type_implement_interface(left_type, right_type) - if is_true { - break - } - } - if right_sym.info is ast.FnType - && c.comptime.comptime_for_method_var == left_name { - is_true = c.table.fn_signature(right_sym.info.func, - skip_receiver: true - type_only: true - ) == c.table.fn_signature(c.comptime.comptime_for_method, - skip_receiver: true - type_only: true - ) - if is_true { - break - } - } else { - is_true = left_type == right_type - if is_true { - break - } - } + is_true = c.check_compatible_types(left_type, left_name, expr) + if is_true { + break } } is_true = if cond.op in [.key_in, .key_is] { is_true } else { !is_true } diff --git a/vlib/v/checker/match.v b/vlib/v/checker/match.v index 6d23206766..8d33203ca7 100644 --- a/vlib/v/checker/match.v +++ b/vlib/v/checker/match.v @@ -18,13 +18,26 @@ fn (mut c Checker) match_expr(mut node ast.MatchExpr) ast.Type { c.expected_expr_type = ast.void_type } } - cond_type := c.expr(mut node.cond) + mut cond_type := ast.void_type + if node.is_comptime { + // for field.name and generic type `T` + if node.cond is ast.SelectorExpr { + c.expr(mut node.cond) + } + cond_type = c.get_expr_type(node.cond) + } else { + cond_type = c.expr(mut node.cond) + } // we setting this here rather than at the end of the method // since it is used in c.match_exprs() it saves checking twice node.cond_type = ast.mktyp(cond_type) if (node.cond is ast.Ident && node.cond.is_mut) || (node.cond is ast.SelectorExpr && node.cond.is_mut) { - c.fail_if_immutable(mut node.cond) + if node.is_comptime { + c.error('`\$match` condition `${node.cond}` can not be mutable', node.cond.pos()) + } else { + c.fail_if_immutable(mut node.cond) + } } if !c.ensure_type_exists(node.cond_type, node.pos) { return ast.void_type @@ -42,7 +55,164 @@ fn (mut c Checker) match_expr(mut node ast.MatchExpr) ast.Type { mut nbranches_with_return := 0 mut nbranches_without_return := 0 mut must_be_option := false + comptime_branch_context_str := if node.is_comptime { c.gen_branch_context_string() } else { '' } + mut comptime_match_branch_result := false + mut comptime_match_found_branch := false + mut comptime_match_cond_value := '' + if node.is_comptime { + if node.cond in [ast.ComptimeType, ast.TypeNode] + || (node.cond is ast.Ident && (c.is_generic_ident(node.cond.name))) { + // must be a type `$match` + c.inside_x_matches_type = true + } else { + // a value `$match`, eval the `node.cond` first + c.expr(mut node.cond) + if !c.type_resolver.is_generic_param_var(node.cond) { + match mut node.cond { + ast.StringLiteral { + comptime_match_cond_value = node.cond.val + } + ast.IntegerLiteral { + comptime_match_cond_value = node.cond.val.str() + } + ast.BoolLiteral { + comptime_match_cond_value = node.cond.val.str() + } + ast.Ident { + mut cond_expr := c.find_definition(node.cond) or { + c.error(err.msg(), node.cond.pos) + return ast.void_type + } + match mut cond_expr { + ast.StringLiteral { + comptime_match_cond_value = cond_expr.val + } + ast.IntegerLiteral { + comptime_match_cond_value = cond_expr.val.str() + } + ast.BoolLiteral { + comptime_match_cond_value = cond_expr.val.str() + } + else { + c.error('`${node.cond}` is not a string/int/bool literal.', + node.cond.pos) + return ast.void_type + } + } + } + ast.SelectorExpr { + if c.comptime.inside_comptime_for && node.cond.field_name in ['name', 'typ'] { + // hack: `typ` is just for bypass the error test, because we don't know it is a type match or a value match righ now + comptime_match_cond_value = c.comptime.comptime_for_field_value.name + } else { + c.error('`${node.cond}` is not `\$for` field.name.', node.cond.pos) + return ast.void_type + } + } + else { + c.error('`\$match` cond only support string/int/bool/ident.', + node.cond.pos()) + return ast.void_type + } + } + } + } + } for mut branch in node.branches { + if node.is_comptime { + // `idx_str` is composed of two parts: + // The first part represents the current context of the branch statement, `comptime_branch_context_str`, formatted like `T=int,X=string,method.name=json` + // The second part indicates the branch's location in the source file. + // This format must match what is in `cgen`. + idx_str := comptime_branch_context_str + '|${c.file.path}|${branch.pos}|' + mut c_str := '' + if !branch.is_else { + if c.inside_x_matches_type { + // type $match + for expr in branch.exprs { + if mut node.cond is ast.ComptimeType { + // $match $int { a {} + branch_type := c.get_expr_type(expr) + comptime_match_branch_result = c.type_resolver.is_comptime_type(branch_type, + node.cond) + c_str = '${expr} == ${c.table.type_to_str(branch_type)}' + } else { + // $match a { $int {} + comptime_match_branch_result = c.check_compatible_types(node.cond_type, + '${node.cond}', expr) + c_str = '${c.table.type_to_str(node.cond_type)} == ${expr}' + } + if comptime_match_branch_result { + break + } + } + } else { + // value $match + for mut expr in branch.exprs { + match mut expr { + ast.Ident { + mut branch_expr := c.find_definition(expr) or { + c.error(err.msg(), expr.pos) + return ast.void_type + } + match mut branch_expr { + ast.StringLiteral { + comptime_match_branch_result = branch_expr.val == comptime_match_cond_value + } + ast.IntegerLiteral { + comptime_match_branch_result = branch_expr.val.str() == comptime_match_cond_value + } + ast.BoolLiteral { + comptime_match_branch_result = branch_expr.val.str() == comptime_match_cond_value + } + else { + c.error('`${expr}` is not a string/int/bool literal.', + expr.pos) + return ast.void_type + } + } + c_str = '${node.cond} == ${expr.name}' + } + ast.SelectorExpr {} + ast.StringLiteral { + c_str = '${node.cond} == ${expr}' + comptime_match_branch_result = comptime_match_cond_value == expr.val + } + ast.IntegerLiteral { + c_str = '${node.cond} == ${expr.val}' + comptime_match_branch_result = comptime_match_cond_value == expr.val.str() + } + ast.BoolLiteral { + c_str = '${node.cond} == ${expr.val}' + comptime_match_branch_result = comptime_match_cond_value == expr.val.str() + } + else { + c.error('`\$match` branch only support string/int/bool types', + node.cond.pos()) + return ast.void_type + } + } + if comptime_match_branch_result { + break + } + } + } + if comptime_match_branch_result { + comptime_match_found_branch = true + } + // set `comptime_is_true` which can be used by `cgen` + c.table.comptime_is_true[idx_str] = ast.ComptTimeCondResult{ + val: comptime_match_branch_result + c_str: c_str + } + } else { + comptime_match_branch_result = !comptime_match_found_branch + c.table.comptime_is_true[idx_str] = ast.ComptTimeCondResult{ + val: comptime_match_branch_result + c_str: '' + } + } + } if node.is_expr { c.stmts_ending_with_expression(mut branch.stmts, c.expected_or_type) } else { @@ -326,6 +496,7 @@ fn (mut c Checker) match_exprs(mut node ast.MatchExpr, cond_type_sym ast.TypeSym c.expected_type = node.expected_type cond_sym := c.table.sym(node.cond_type) mut enum_ref_checked := false + mut is_comptime_value_match := false // branch_exprs is a histogram of how many times // an expr was used in the match mut branch_exprs := map[string]int{} @@ -335,7 +506,7 @@ fn (mut c Checker) match_exprs(mut node ast.MatchExpr, cond_type_sym ast.TypeSym for k, mut expr in branch.exprs { mut key := '' // TODO: investigate why enums are different here: - if expr !is ast.EnumVal { + if expr !is ast.EnumVal && !(node.is_comptime && expr is ast.ComptimeType) { // ensure that the sub expressions of the branch are actually checked, before anything else: _ := c.expr(mut expr) } @@ -351,7 +522,8 @@ fn (mut c Checker) match_exprs(mut node ast.MatchExpr, cond_type_sym ast.TypeSym && node.cond.field_name == 'typ' } } - if mut expr is ast.TypeNode && cond_sym.is_primitive() && !is_comptime { + if mut expr is ast.TypeNode && cond_sym.is_primitive() && !is_comptime + && !node.is_comptime { c.error('matching by type can only be done for sum types, generics, interfaces, `${node.cond}` is none of those', branch.pos) } @@ -412,7 +584,7 @@ fn (mut c Checker) match_exprs(mut node ast.MatchExpr, cond_type_sym ast.TypeSym } continue } - is_type_node := expr is ast.TypeNode + is_type_node := expr is ast.TypeNode || expr is ast.ComptimeType match mut expr { ast.TypeNode { key = c.table.type_to_str(expr.typ) @@ -439,46 +611,98 @@ fn (mut c Checker) match_exprs(mut node ast.MatchExpr, cond_type_sym ast.TypeSym c.expected_type = node.cond_type if is_type_node { c.inside_x_matches_type = true + } else { + if node.is_comptime { + is_comptime_value_match = true + } } - expr_type := c.expr(mut expr) - if expr_type.idx() == 0 { - // parser failed, stop checking - return - } - expr_type_sym := c.table.sym(expr_type) - if cond_type_sym.kind == .interface { - // TODO - // This generates a memory issue with TCC - // Needs to be checked later when TCC errors are fixed - // Current solution is to move expr.pos() to its own statement - // c.type_implements(expr_type, c.expected_type, expr.pos()) - expr_pos := expr.pos() - if c.type_implements(expr_type, c.expected_type, expr_pos) { - if !expr_type.is_any_kind_of_pointer() && !c.inside_unsafe { - if expr_type_sym.kind != .interface { - c.mark_as_referenced(mut &branch.exprs[k], true) + if node.is_comptime { + if is_type_node { + if is_comptime_value_match { + // type branch in a value match + c.error('can not matching a type in a value `\$match`', expr.pos()) + return + } + } else if c.inside_x_matches_type { + // value branch in a type match + if expr in [ast.IntegerLiteral, ast.BoolLiteral, ast.StringLiteral] { + c.error('can not matching a value in a type `\$match`', expr.pos()) + return + } + } + if !is_type_node { + // value check should match cond's type + if expr is ast.IntegerLiteral { + if mut node.cond is ast.ComptimeType { + if node.cond.kind != .int { + c.error('can not matching a int value(`${expr}`) in a non int type `\$match`, `${node.cond}` type is `${node.cond.kind}`', + expr.pos()) + return + } + } else if node.cond_type !in ast.integer_type_idxs { + c.error('can not matching a int value(`${expr}`) in a non int type `\$match`, `${node.cond}` type is `${c.table.type_to_str(node.cond_type)}`', + expr.pos()) + return + } + } else if expr is ast.BoolLiteral && node.cond_type != ast.bool_type { + c.error('can not matching a bool value(`${expr}`) in a non bool type `\$match`, `${node.cond}` type is `${c.table.type_to_str(node.cond_type)}`', + expr.pos()) + return + } else if expr is ast.StringLiteral { + if mut node.cond is ast.ComptimeType { + if node.cond.kind != .string { + c.error('can not matching a string value(`${expr}`) in a non string type `\$match`, `${node.cond}` type is `${node.cond.kind}`', + expr.pos()) + return + } + } else if node.cond_type != ast.string_type { + c.error('can not matching a string value(`${expr}`) in a non string type `\$match`, `${node.cond}` type is `${c.table.type_to_str(node.cond_type)}`', + expr.pos()) + return } } } - } else if cond_type_sym.info is ast.SumType { - if expr_type !in cond_type_sym.info.variants { + } else { + expr_type := c.expr(mut expr) + if expr_type.idx() == 0 { + // parser failed, stop checking + return + } + expr_type_sym := c.table.sym(expr_type) + if cond_type_sym.kind == .interface { + // TODO + // This generates a memory issue with TCC + // Needs to be checked later when TCC errors are fixed + // Current solution is to move expr.pos() to its own statement + // c.type_implements(expr_type, c.expected_type, expr.pos()) + expr_pos := expr.pos() + if c.type_implements(expr_type, c.expected_type, expr_pos) { + if !expr_type.is_any_kind_of_pointer() && !c.inside_unsafe { + if expr_type_sym.kind != .interface { + c.mark_as_referenced(mut &branch.exprs[k], true) + } + } + } + } else if cond_type_sym.info is ast.SumType { + if expr_type !in cond_type_sym.info.variants { + expr_str := c.table.type_to_str(expr_type) + expect_str := c.table.type_to_str(node.cond_type) + sumtype_variant_names := cond_type_sym.info.variants.map(c.table.type_to_str_using_aliases(it, + {})) + suggestion := util.new_suggestion(expr_str, sumtype_variant_names) + c.error(suggestion.say('`${expect_str}` has no variant `${expr_str}`'), + expr.pos()) + } + } else if cond_type_sym.info is ast.Alias && expr_type_sym.info is ast.Struct { expr_str := c.table.type_to_str(expr_type) expect_str := c.table.type_to_str(node.cond_type) - sumtype_variant_names := cond_type_sym.info.variants.map(c.table.type_to_str_using_aliases(it, - {})) - suggestion := util.new_suggestion(expr_str, sumtype_variant_names) - c.error(suggestion.say('`${expect_str}` has no variant `${expr_str}`'), + c.error('cannot match alias type `${expect_str}` with `${expr_str}`', expr.pos()) + } else if !c.check_types(expr_type, node.cond_type) && !is_comptime { + expr_str := c.table.type_to_str(expr_type) + expect_str := c.table.type_to_str(node.cond_type) + c.error('cannot match `${expect_str}` with `${expr_str}`', expr.pos()) } - } else if cond_type_sym.info is ast.Alias && expr_type_sym.info is ast.Struct { - expr_str := c.table.type_to_str(expr_type) - expect_str := c.table.type_to_str(node.cond_type) - c.error('cannot match alias type `${expect_str}` with `${expr_str}`', - expr.pos()) - } else if !c.check_types(expr_type, node.cond_type) && !is_comptime { - expr_str := c.table.type_to_str(expr_type) - expect_str := c.table.type_to_str(node.cond_type) - c.error('cannot match `${expect_str}` with `${expr_str}`', expr.pos()) } branch_exprs[key] = val + 1 } @@ -596,7 +820,7 @@ fn (mut c Checker) match_exprs(mut node ast.MatchExpr, cond_type_sym ast.TypeSym } return } - if has_else { + if has_else || node.is_comptime { return } mut err_details := 'match must be exhaustive' diff --git a/vlib/v/checker/tests/comptime_match_cond_cannot_mut.out b/vlib/v/checker/tests/comptime_match_cond_cannot_mut.out new file mode 100644 index 0000000000..1251c37ed7 --- /dev/null +++ b/vlib/v/checker/tests/comptime_match_cond_cannot_mut.out @@ -0,0 +1,14 @@ +vlib/v/checker/tests/comptime_match_cond_cannot_mut.vv:3:9: error: `x` is mut and may have changed since its definition + 1 | fn main() { + 2 | mut x := 1 + 3 | $match x { + | ^ + 4 | 1 { + 5 | println('1') +vlib/v/checker/tests/comptime_match_cond_cannot_mut.vv:13:13: error: `$match` condition `y` can not be mutable + 11 | + 12 | y := 100 + 13 | $match mut y { + | ^ + 14 | 100 { + 15 | println('100') diff --git a/vlib/v/checker/tests/comptime_match_cond_cannot_mut.vv b/vlib/v/checker/tests/comptime_match_cond_cannot_mut.vv new file mode 100644 index 0000000000..4263171681 --- /dev/null +++ b/vlib/v/checker/tests/comptime_match_cond_cannot_mut.vv @@ -0,0 +1,18 @@ +fn main() { + mut x := 1 + $match x { + 1 { + println('1') + } + 2 { + println('2') + } + } + + y := 100 + $match mut y { + 100 { + println('100') + } + } +} diff --git a/vlib/v/checker/tests/comptime_match_value_different_type.out b/vlib/v/checker/tests/comptime_match_value_different_type.out new file mode 100644 index 0000000000..e4449b727a --- /dev/null +++ b/vlib/v/checker/tests/comptime_match_value_different_type.out @@ -0,0 +1,14 @@ +vlib/v/checker/tests/comptime_match_value_different_type.vv:7:3: error: can not matching a string value(`'100'`) in a non string type `$match`, `x` type is `int` + 5 | println('int 100') + 6 | } + 7 | '100' { + | ~~~~~ + 8 | println('string 100') + 9 | } +vlib/v/checker/tests/comptime_match_value_different_type.vv:18:3: error: can not matching a string value(`'1'`) in a non string type `$match`, `$int` type is `int` + 16 | println('IntegerLiteral') + 17 | } + 18 | '1' { + | ~~~ + 19 | println('StringLiteral') + 20 | } diff --git a/vlib/v/checker/tests/comptime_match_value_different_type.vv b/vlib/v/checker/tests/comptime_match_value_different_type.vv new file mode 100644 index 0000000000..3d9876a198 --- /dev/null +++ b/vlib/v/checker/tests/comptime_match_value_different_type.vv @@ -0,0 +1,25 @@ +fn match_value_different_type() { + x := 100 + $match x { + 100 { + println('int 100') + } + '100' { + println('string 100') + } + } +} + +fn match_type_different_literal() { + $match $int { + 1 { + println('IntegerLiteral') + } + '1' { + println('StringLiteral') + } + true { + println('BoolLiteral') + } + } +} diff --git a/vlib/v/checker/tests/comptime_match_value_type_mix_check.out b/vlib/v/checker/tests/comptime_match_value_type_mix_check.out new file mode 100644 index 0000000000..c24e75bc8d --- /dev/null +++ b/vlib/v/checker/tests/comptime_match_value_type_mix_check.out @@ -0,0 +1,14 @@ +vlib/v/checker/tests/comptime_match_value_type_mix_check.vv:9:3: error: can not matching a type in a value `$match` + 7 | println('value check') + 8 | } + 9 | int { + | ~~~ + 10 | println('type check') + 11 | } +vlib/v/checker/tests/comptime_match_value_type_mix_check.vv:21:3: error: can not matching a value in a type `$match` + 19 | println('type check') + 20 | } + 21 | 100 { + | ~~~ + 22 | println('value check') + 23 | } diff --git a/vlib/v/checker/tests/comptime_match_value_type_mix_check.vv b/vlib/v/checker/tests/comptime_match_value_type_mix_check.vv new file mode 100644 index 0000000000..aefa4b8f4b --- /dev/null +++ b/vlib/v/checker/tests/comptime_match_value_type_mix_check.vv @@ -0,0 +1,25 @@ +module main + +fn type_check_in_a_value_match() { + x := 100 + $match x { + 100 { + println('value check') + } + int { + println('type check') + } + } +} + +fn value_check_in_a_type_match() { + x := 100 + $match x { + int { + println('type check') + } + 100 { + println('value check') + } + } +} diff --git a/vlib/v/fmt/fmt.v b/vlib/v/fmt/fmt.v index 283150aa1e..852db9ca50 100644 --- a/vlib/v/fmt/fmt.v +++ b/vlib/v/fmt/fmt.v @@ -2842,7 +2842,7 @@ pub fn (mut f Fmt) map_init(node ast.MapInit) { f.write('}') } -fn (mut f Fmt) match_branch(branch ast.MatchBranch, single_line bool) { +fn (mut f Fmt) match_branch(branch ast.MatchBranch, single_line bool, is_comptime bool) { if !branch.is_else { // normal branch f.is_mbranch_expr = true @@ -2864,7 +2864,11 @@ fn (mut f Fmt) match_branch(branch ast.MatchBranch, single_line bool) { f.is_mbranch_expr = false } else { // else branch - f.write('else') + if is_comptime { + f.write('\$else') + } else { + f.write('else') + } } if branch.stmts.len == 0 { f.writeln(' {}') @@ -2888,7 +2892,8 @@ fn (mut f Fmt) match_branch(branch ast.MatchBranch, single_line bool) { } pub fn (mut f Fmt) match_expr(node ast.MatchExpr) { - f.write('match ') + dollar := if node.is_comptime { '$' } else { '' } + f.write('${dollar}match ') f.expr(node.cond) f.writeln(' {') f.indent++ @@ -2913,10 +2918,10 @@ pub fn (mut f Fmt) match_expr(node ast.MatchExpr) { else_idx = i continue } - f.match_branch(branch, single_line) + f.match_branch(branch, single_line, node.is_comptime) } if else_idx >= 0 { - f.match_branch(node.branches[else_idx], single_line) + f.match_branch(node.branches[else_idx], single_line, node.is_comptime) } f.indent-- f.write('}') diff --git a/vlib/v/gen/c/cgen.v b/vlib/v/gen/c/cgen.v index 5fa5de541e..f8b7a3f03f 100644 --- a/vlib/v/gen/c/cgen.v +++ b/vlib/v/gen/c/cgen.v @@ -3823,7 +3823,11 @@ fn (mut g Gen) expr(node_ ast.Expr) { g.map_init(node) } ast.MatchExpr { - g.match_expr(node) + if node.is_comptime { + g.comptime_match(node) + } else { + g.match_expr(node) + } } ast.NodeError {} ast.Nil { diff --git a/vlib/v/gen/c/comptime.v b/vlib/v/gen/c/comptime.v index 6ea641ec42..e48d9044e8 100644 --- a/vlib/v/gen/c/comptime.v +++ b/vlib/v/gen/c/comptime.v @@ -946,193 +946,102 @@ fn (mut g Gen) comptime_selector_type(node ast.SelectorExpr) ast.Type { return node.expr_type } -fn (mut g Gen) comptime_if_to_ifdef(name string, is_comptime_option bool) !string { - match name { - // platforms/os-es: - 'windows' { - return '_WIN32' +fn (mut g Gen) comptime_match(node ast.MatchExpr) { + tmp_var := g.new_tmp_var() + is_opt_or_result := node.return_type.has_option_or_result() + line := if node.is_expr { + stmt_str := g.go_before_last_stmt() + g.write(util.tabs(g.indent)) + styp := g.styp(node.return_type) + g.writeln('${styp} ${tmp_var};') + stmt_str + } else { + '' + } + + mut comptime_branch_context_str := g.gen_branch_context_string() + mut is_true := ast.ComptTimeCondResult{} + for i, branch in node.branches { + // `idx_str` is composed of two parts: + // The first part represents the current context of the branch statement, `comptime_branch_context_str`, formatted like `T=int,X=string,method.name=json` + // The second part indicates the branch's location in the source file. + // This format must match what is in `checker`. + idx_str := comptime_branch_context_str + '|${g.file.path}|${branch.pos}|' + if comptime_is_true := g.table.comptime_is_true[idx_str] { + // `g.table.comptime_is_true` are the branch condition results set by `checker` + is_true = comptime_is_true + } else { + g.error('checker error: match branch result idx string not found => [${idx_str}]', + branch.pos) + return } - 'ios' { - return '__TARGET_IOS__' - } - 'macos' { - return '__APPLE__' - } - 'mach' { - return '__MACH__' - } - 'darwin' { - return '__DARWIN__' - } - 'hpux' { - return '__HPUX__' - } - 'gnu' { - return '__GNU__' - } - 'qnx' { - return '__QNX__' - } - 'linux' { - return '__linux__' - } - 'serenity' { - return '__serenity__' - } - 'plan9' { - return '__plan9__' - } - 'vinix' { - return '__vinix__' - } - 'freebsd' { - return '__FreeBSD__' - } - 'openbsd' { - return '__OpenBSD__' - } - 'netbsd' { - return '__NetBSD__' - } - 'bsd' { - return '__BSD__' - } - 'dragonfly' { - return '__DragonFly__' - } - 'android' { - return '__ANDROID__' - } - 'termux' { - // Note: termux is running on Android natively so __ANDROID__ will also be defined - return '__TERMUX__' - } - 'solaris' { - return '__sun' - } - 'haiku' { - return '__HAIKU__' - } - // - 'js' { - return '_VJS' - } - 'wasm32_emscripten' { - return '__EMSCRIPTEN__' - } - 'native' { - return '_VNATIVE' // when using the native backend, cgen is inactive - } - // compilers: - 'gcc' { - return '__V_GCC__' - } - 'tinyc' { - return '__TINYC__' - } - 'clang' { - return '__clang__' - } - 'mingw' { - return '__MINGW32__' - } - 'msvc' { - return '_MSC_VER' - } - 'cplusplus' { - return '__cplusplus' - } - // other: - 'threads' { - return '__VTHREADS__' - } - 'gcboehm' { - return '_VGCBOEHM' - } - 'debug' { - return '_VDEBUG' - } - 'prod' { - return '_VPROD' - } - 'profile' { - return '_VPROFILE' - } - 'test' { - return '_VTEST' - } - 'glibc' { - return '__GLIBC__' - } - 'prealloc' { - return '_VPREALLOC' - } - 'no_bounds_checking' { - return 'CUSTOM_DEFINE_no_bounds_checking' - } - 'freestanding' { - return '_VFREESTANDING' - } - 'autofree' { - return '_VAUTOFREE' - } - // architectures: - 'amd64' { - return '__V_amd64' - } - 'aarch64', 'arm64' { - return '__V_arm64' - } - 'arm32' { - return '__V_arm32' - } - 'i386' { - return '__V_x86' - } - 'rv64', 'riscv64' { - return '__V_rv64' - } - 'rv32', 'riscv32' { - return '__V_rv32' - } - 's390x' { - return '__V_s390x' - } - 'ppc64le' { - return '__V_ppc64le' - } - 'loongarch64' { - return '__V_loongarch64' - } - // bitness: - 'x64' { - return 'TARGET_IS_64BIT' - } - 'x32' { - return 'TARGET_IS_32BIT' - } - // endianness: - 'little_endian' { - return 'TARGET_ORDER_IS_LITTLE' - } - 'big_endian' { - return 'TARGET_ORDER_IS_BIG' - } - 'fast_math' { - if g.pref.ccompiler_type == .msvc { - // turned on by: `-cflags /fp:fast` - return '_M_FP_FAST' + if !branch.is_else { + if i == 0 { + g.write('#if ') + } else { + g.write('#elif ') } - // turned on by: `-cflags -ffast-math` - return '__FAST_MATH__' - } - else { - if is_comptime_option - || (g.pref.compile_defines_all.len > 0 && name in g.pref.compile_defines_all) { - return 'CUSTOM_DEFINE_${name}' + // directly use `checker` evaluate results + g.writeln('${is_true.val}') + $if debug_comptime_branch_context ? { + g.writeln('/* | generic=[${comptime_branch_context_str}] */') + } + } else { + g.writeln('#else') + } + + if node.is_expr { + len := branch.stmts.len + if len > 0 { + last := branch.stmts.last() as ast.ExprStmt + if len > 1 { + g.indent++ + g.writeln('{') + g.stmts(branch.stmts[..len - 1]) + g.set_current_pos_as_last_stmt_pos() + prev_skip_stmt_pos := g.skip_stmt_pos + g.skip_stmt_pos = true + if is_opt_or_result { + tmp_var2 := g.new_tmp_var() + g.write('{ ${g.base_type(node.return_type)} ${tmp_var2} = ') + g.stmt(last) + g.writeln('_result_ok(&(${g.base_type(node.return_type)}[]) { ${tmp_var2} }, (_result*)(&${tmp_var}), sizeof(${g.base_type(node.return_type)}));') + g.writeln('}') + } else { + g.write('\t${tmp_var} = ') + g.stmt(last) + } + g.skip_stmt_pos = prev_skip_stmt_pos + g.writeln2(';', '}') + g.indent-- + } else { + g.indent++ + g.set_current_pos_as_last_stmt_pos() + prev_skip_stmt_pos := g.skip_stmt_pos + g.skip_stmt_pos = true + if is_opt_or_result { + tmp_var2 := g.new_tmp_var() + g.write('{ ${g.base_type(node.return_type)} ${tmp_var2} = ') + g.stmt(last) + g.writeln('_result_ok(&(${g.base_type(node.return_type)}[]) { ${tmp_var2} }, (_result*)(&${tmp_var}), sizeof(${g.base_type(node.return_type)}));') + g.writeln('}') + } else { + g.write('${tmp_var} = ') + g.stmt(last) + } + g.skip_stmt_pos = prev_skip_stmt_pos + g.writeln(';') + g.indent-- + } + } + } else { + if is_true.val || g.pref.output_cross_c { + g.stmts(branch.stmts) } - return error('bad os ifdef name "${name}"') // should never happen, caught in the checker } } - return error('none') + g.writeln('#endif') + if node.is_expr { + g.write('${line}${tmp_var}') + } } diff --git a/vlib/v/parser/expr.v b/vlib/v/parser/expr.v index 1f4292771f..d6bb02c015 100644 --- a/vlib/v/parser/expr.v +++ b/vlib/v/parser/expr.v @@ -128,6 +128,9 @@ fn (mut p Parser) check_expr(precedence int) !ast.Expr { .key_if { return p.if_expr(true, false) } + .key_match { + return p.match_expr(true) + } else { return p.unexpected_with_pos(p.peek_tok.pos(), got: '`$`' @@ -184,7 +187,7 @@ fn (mut p Parser) check_expr(precedence int) !ast.Expr { if p.peek_tok.kind in [.lpar, .lsbr] && p.peek_tok.is_next_to(p.tok) { node = p.call_expr(p.language, p.mod) } else { - node = p.match_expr() + node = p.match_expr(false) } } .key_select { diff --git a/vlib/v/parser/if_match.v b/vlib/v/parser/if_match.v index 624044ac82..aeb4375f6b 100644 --- a/vlib/v/parser/if_match.v +++ b/vlib/v/parser/if_match.v @@ -244,8 +244,12 @@ fn (mut p Parser) is_match_sumtype_type() bool { && next_next_tok.lit.len > 0 && next_next_tok.lit[0].is_capital())) } -fn (mut p Parser) match_expr() ast.MatchExpr { - match_first_pos := p.tok.pos() +fn (mut p Parser) match_expr(is_comptime bool) ast.MatchExpr { + mut match_first_pos := p.tok.pos() + if is_comptime { + p.next() // `$` + match_first_pos = p.prev_tok.pos().extend(p.tok.pos()) + } old_inside_match := p.inside_match p.inside_match = true p.check(.key_match) @@ -265,6 +269,15 @@ fn (mut p Parser) match_expr() ast.MatchExpr { p.open_scope() // final else mut is_else := false + if is_comptime { + if p.tok.kind == .key_else { + p.error('use `\$else` instead of `else` in compile-time `match` branches') + return ast.MatchExpr{} + } + if p.tok.kind != .rcbr && p.peek_tok.kind == .key_else { + p.check(.dollar) + } + } if p.tok.kind == .key_else { is_else = true p.next() @@ -382,6 +395,7 @@ fn (mut p Parser) match_expr() ast.MatchExpr { // return ast.StructInit{} pos.update_last_line(p.prev_tok.line_nr) return ast.MatchExpr{ + is_comptime: is_comptime branches: branches cond: cond is_sum_type: is_sum_type diff --git a/vlib/v/parser/parser.v b/vlib/v/parser/parser.v index e13ebd5487..6cbb844d97 100644 --- a/vlib/v/parser/parser.v +++ b/vlib/v/parser/parser.v @@ -957,6 +957,15 @@ fn (mut p Parser) stmt(is_top_level bool) ast.Stmt { .key_for { return p.comptime_for() } + .key_match { + mut pos := p.tok.pos() + expr := p.match_expr(true) + pos.update_last_line(p.prev_tok.line_nr) + return ast.ExprStmt{ + expr: expr + pos: pos + } + } .name { // handles $dbg directly without registering token if p.peek_tok.lit == 'dbg' { diff --git a/vlib/v/tests/comptime/comptime_match_assign_test.v b/vlib/v/tests/comptime/comptime_match_assign_test.v new file mode 100644 index 0000000000..fa028d79d7 --- /dev/null +++ b/vlib/v/tests/comptime/comptime_match_assign_test.v @@ -0,0 +1,62 @@ +fn test_comptime_match_assign() { + os := 'windows' + x := $match os { + 'linux' { 'linux' } + 'windows' { 'windows' } + $else { 'unknown' } + } + + assert x == 'windows' + + i := 123 + y := $match i { + 1 { '1' } + 2 { '2' } + 123 { '123' } + $else { 'unknown' } + } + assert y == '123' + + j := true + z := $match j { + true { 'T' } + false { 'F' } + } + + assert z == 'T' +} + +fn test_comptime_match_assign_reverse() { + os1 := 'windows' + os2 := 'linux' + os3 := 'macos' + x := $match 'windows' { + os1 { 'w' } + os2 { 'l' } + os3 { 'm' } + $else { 'unknown' } + } + assert x == 'w' + + b1 := true + b2 := false + b3 := true + y := $match false { + b1 { 'b1' } + b2 { 'b2' } + b3 { 'b3' } + $else { 'unknown' } + } + assert y == 'b2' + + i1 := 123 + i2 := 245 + i3 := 1023 + z := $match 1024 { + i1 { '123' } + i2 { '245' } + i3 { '1023' } + $else { 'unknown' } + } + assert z == 'unknown' +} diff --git a/vlib/v/tests/comptime/comptime_match_for_field_type_test.v b/vlib/v/tests/comptime/comptime_match_for_field_type_test.v new file mode 100644 index 0000000000..08ea502320 --- /dev/null +++ b/vlib/v/tests/comptime/comptime_match_for_field_type_test.v @@ -0,0 +1,47 @@ +struct My { + a int + b f64 + c string +} + +fn test_comptime_match_for_field_type() { + x := My{} + + mut result := '' + + $for f in x.fields { + f_name := f.name + $match f.typ { + int { + result += '${f_name}=int,' + } + f64 { + result += '${f_name}=f64,' + } + string { + result += '${f_name}=string,' + } + $else { + result += '${f_name}=unknown,' + } + } + } + assert result == 'a=int,b=f64,c=string,' +} + +fn test_comptime_match_for_field_type_reverse() { + x := My{} + a := 100 + + mut result := '' + + $for f in x.fields { + f_name := f.name + $match $int { + f.typ { + result += '${f_name}=int,' + } + } + } + assert result == 'a=int,' +} diff --git a/vlib/v/tests/comptime/comptime_match_for_field_value_test.v b/vlib/v/tests/comptime/comptime_match_for_field_value_test.v new file mode 100644 index 0000000000..ad683866a1 --- /dev/null +++ b/vlib/v/tests/comptime/comptime_match_for_field_value_test.v @@ -0,0 +1,27 @@ +struct My { + a int + b string + c f64 +} + +fn test_comptime_match_for_field_value() { + x := My{} + mut result := '' + $for f in x.fields { + $match f.name { + 'a' { + result += 'a' + } + 'b' { + result += 'b' + } + 'c' { + result += 'c' + } + $else { + result += '0' + } + } + } + assert result == 'abc' +} diff --git a/vlib/v/tests/comptime/comptime_match_generic_type_test.v b/vlib/v/tests/comptime/comptime_match_generic_type_test.v new file mode 100644 index 0000000000..7be979187c --- /dev/null +++ b/vlib/v/tests/comptime/comptime_match_generic_type_test.v @@ -0,0 +1,40 @@ +fn func[T](val T) string { + mut result := '' + $match T { + int { + result += 'int' + } + f64 { + result += 'f64' + } + string { + result += 'string' + } + $else { + result += 'unknown' + } + } + + $match val { + int { + result += ',int' + } + f64 { + result += ',f64' + } + string { + result += ',string' + } + $else { + result += ',unknown' + } + } + return result +} + +fn test_comptime_match_generic_type() { + assert func(100) == 'int,int' + assert func(1.1) == 'f64,f64' + assert func('1') == 'string,string' + assert func(`a`) == 'unknown,unknown' +} diff --git a/vlib/v/tests/comptime/comptime_match_type_2_test.v b/vlib/v/tests/comptime/comptime_match_type_2_test.v new file mode 100644 index 0000000000..2f0ad605cd --- /dev/null +++ b/vlib/v/tests/comptime/comptime_match_type_2_test.v @@ -0,0 +1,22 @@ +// This is a duplicate test of `comptime_match_type_test.v`, but with `$match` instead of a `match` +struct Test { + a int + b []int + c map[int]string + d []?int +} + +fn test_main() { + mut i := 1 + $for f in Test.fields { + type_name := typeof(f.$(f.name)).name + $match f.typ { + int { assert i == 1, '1. ${f.name} is ${type_name}' } + []int { assert i == 2, '2. ${f.name} is ${type_name}' } + map[int]string { assert i == 3, '3. ${f.name} is ${type_name}' } + []?int { assert i == 4, '4. ${f.name} is ${type_name}' } + $else {} + } + i++ + } +} diff --git a/vlib/v/tests/comptime/comptime_match_type_check_test.v b/vlib/v/tests/comptime/comptime_match_type_check_test.v new file mode 100644 index 0000000000..f49c09d487 --- /dev/null +++ b/vlib/v/tests/comptime/comptime_match_type_check_test.v @@ -0,0 +1,53 @@ +fn test_comptime_match_type_check() { + x := 100 + mut result := '' + $match x { + f64 { + result += 'f64' + } + u32 { + result += 'u32' + } + $int { + result += '\$int' + } + int, i32 { + result += 'int' + } + $else { + result += 'unknown' + } + } + + assert result == '\$int' +} + +fn test_comptime_match_type_check_reverse() { + a := 100 + b := 200 + c := 300 + x := '123' + y := u64(22) + z := 1.2 + + mut result := '' + $match $int { + a, b { + result += 'a|b' + } + c { + result += 'c' + } + x { + result += 'x' + } + y { + result += 'y' + } + z { + result += 'z' + } + } + + assert result == 'a|b' +} diff --git a/vlib/v/tests/comptime/comptime_match_value_check_test.v b/vlib/v/tests/comptime/comptime_match_value_check_test.v new file mode 100644 index 0000000000..3e974ef082 --- /dev/null +++ b/vlib/v/tests/comptime/comptime_match_value_check_test.v @@ -0,0 +1,114 @@ +const version = 123 +const other = 456 + +fn test_comptime_match_value_check() { + x := version + mut result := '' + + $match x { + 1 { + result += 'v1' + } + 2 { + result += 'v2' + } + 3 { + result += 'v3' + } + 123 { + result += 'v123' + } + $else { + result += 'unknown' + } + } + assert result == 'v123' + + result = '' + y := true + $match y { + true { + result += 'true' + } + false { + result += 'false' + } + } + assert result == 'true' + + result = '' + z := 'abc' + $match z { + '123' { + result += 'a' + } + 'abc' { + result += 'b' + } + $else { + result += 'c' + } + } + assert result == 'b' +} + +fn test_comptime_match_value_check_reverse() { + x := version + y := 124 + z := 125 + + mut result := '' + $match 124 { + x { + result += 'x' + } + y { + result += 'y' + } + z { + result += 'z' + } + } + + assert result == 'y' + + result = '' + a := true + b := true + c := false + $match true { + a { + result += 'a' + } + b { + result += 'b' + } + c { + result += 'c' + } + $else { + result += 'else' + } + } + assert result == 'a' + + result = '' + s1 := '123' + s2 := 'abc' + s3 := 'kml' + $match 'abc' { + s1 { + result += 'a' + } + s2 { + result += 'b' + } + s3 { + result += 'c' + } + $else { + result += 'else' + } + } + assert result == 'b' +}