From d6eb7a59747f2fa9a524a9207b16fa62ab5793a6 Mon Sep 17 00:00:00 2001 From: SheatNoisette <45624871+SheatNoisette@users.noreply.github.com> Date: Tue, 6 Jan 2026 07:08:16 +0100 Subject: [PATCH] wasm: implement basic string operations (#26260) --- vlib/builtin/wasm/string.v | 58 ++++++++++++++ vlib/v/gen/wasm/gen.v | 72 ++++++++++++++++-- vlib/v/gen/wasm/ops.v | 110 +++++++++++++++------------ vlib/v/gen/wasm/tests/builtin.vv | 67 ++++++++++++++++ vlib/v/gen/wasm/tests/builtin.vv.out | 21 +++++ vlib/v/gen/wasm/tests/match.vv | 6 +- vlib/v/gen/wasm/tests/match.vv.out | 4 +- 7 files changed, 277 insertions(+), 61 deletions(-) diff --git a/vlib/builtin/wasm/string.v b/vlib/builtin/wasm/string.v index 465d02477..e0986d5ce 100644 --- a/vlib/builtin/wasm/string.v +++ b/vlib/builtin/wasm/string.v @@ -1,7 +1,65 @@ module builtin +// String: This is a minimal implementation of the "string" builtin until the WASM backend +// is able to compile the original builtin implementation + pub struct string { pub: str &u8 len int } + +// Concatenation operator: var += str / str + str +// Note: This will alloc a new string with the content of these two strings +pub fn (s string) + (other string) string { + if s.len == 0 { + return other + } + if other.len == 0 { + return s + } + total_len := s.len + other.len + result_ptr := unsafe { malloc(total_len) } + if result_ptr == 0 { + panic('string.+: malloc failed') + } + unsafe { + vmemcpy(result_ptr, s.str, s.len) + vmemcpy(result_ptr + s.len, other.str, other.len) + } + return string{ + str: result_ptr + len: total_len + } +} + +// Equality comparison: checks if two strings are identical +pub fn (s string) == (other string) bool { + if s.len != other.len { + return false + } + + for i in 0 .. s.len { + if s[i] != other[i] { + return false + } + } + + return true +} + +// Less-than comparison: lexicographically compares two strings +pub fn (s string) < (other string) bool { + // Taken from the C Backend + for i in 0 .. s.len { + if i >= other.len || s[i] > other[i] { + return false + } else if s[i] < other[i] { + return true + } + } + if s.len < other.len { + return true + } + return false +} diff --git a/vlib/v/gen/wasm/gen.v b/vlib/v/gen/wasm/gen.v index d82fa9dbe..2a0455059 100644 --- a/vlib/v/gen/wasm/gen.v +++ b/vlib/v/gen/wasm/gen.v @@ -411,6 +411,60 @@ pub fn (mut g Gen) handle_ptr_arithmetic(typ ast.Type) { } } +fn (mut g Gen) handle_string_operation(op token.Kind) { + left_tmp := g.func.new_local_named(.i32_t, '__tmp.left') + right_tmp := g.func.new_local_named(.i32_t, '__tmp.right') + g.func.local_set(right_tmp) + g.func.local_set(left_tmp) + + match op { + .plus { + ret_var := g.new_local('', ast.string_type) + g.ref(ret_var) + g.func.local_get(left_tmp) + g.func.local_get(right_tmp) + g.func.call('string.+') + g.get(ret_var) + } + .eq { + g.func.local_get(left_tmp) + g.func.local_get(right_tmp) + g.func.call('string.==') + } + .ne { + g.func.local_get(left_tmp) + g.func.local_get(right_tmp) + g.func.call('string.==') + g.func.eqz(.i32_t) + } + .lt { + g.func.local_get(left_tmp) + g.func.local_get(right_tmp) + g.func.call('string.<') + } + .gt { + g.func.local_get(right_tmp) + g.func.local_get(left_tmp) + g.func.call('string.<') + } + .le { + g.func.local_get(right_tmp) + g.func.local_get(left_tmp) + g.func.call('string.<') + g.func.eqz(.i32_t) + } + .ge { + g.func.local_get(left_tmp) + g.func.local_get(right_tmp) + g.func.call('string.<') + g.func.eqz(.i32_t) + } + else { + g.w_error('unsupported string operation: `${op}`') + } + } +} + pub fn (mut g Gen) infix_expr(node ast.InfixExpr, expected ast.Type) { if node.op in [.logical_or, .and] { temp := g.func.new_local_named(.i32_t, '__tmp') @@ -601,20 +655,28 @@ fn (mut g Gen) match_branch_exprs(node ast.MatchExpr, expected ast.Type, unpacke mut is_last_branch := branch_idx + 1 >= node.branches.len mut is_last_expr := expr_idx + 1 >= branch.exprs.len - wasm_type := g.as_numtype(g.get_wasm_type(node.cond_type)) - expr := branch.exprs[expr_idx] if expr is ast.RangeExpr { + wasm_type := g.as_numtype(g.get_wasm_type(node.cond_type)) is_signed := node.cond_type.is_signed() g.expr(node.cond, node.cond_type) g.expr(expr.high, node.cond_type) g.func.le(wasm_type, is_signed) } else { - g.expr(node.cond, node.cond_type) - g.expr(expr, node.cond_type) - g.func.eq(wasm_type) + if g.is_param_type(node.cond_type) { + // Param types -> strings etc + g.expr(node.cond, node.cond_type) + g.expr(expr, node.cond_type) + g.infix_from_typ(node.cond_type, .eq) + } else { + // Numeric types -> direct comparison + wasm_type := g.as_numtype(g.get_wasm_type(node.cond_type)) + g.expr(node.cond, node.cond_type) + g.expr(expr, node.cond_type) + g.func.eq(wasm_type) + } } blk := g.func.c_if([], unpacked_params) diff --git a/vlib/v/gen/wasm/ops.v b/vlib/v/gen/wasm/ops.v index 4e45ce4f6..c50820b28 100644 --- a/vlib/v/gen/wasm/ops.v +++ b/vlib/v/gen/wasm/ops.v @@ -85,68 +85,78 @@ pub fn (mut g Gen) get_wasm_type(typ_ ast.Type) wasm.ValType { g.w_error("get_wasm_type: unreachable type '${*g.table.sym(typ)}' ${ts.info}") } +pub fn (mut g Gen) infix_param_type(typ ast.Type, op token.Kind) { + match typ { + ast.string_type { + g.handle_string_operation(op) + } + else { + eprintln(*g.table.sym(typ)) + panic('unimplemented infix operation for type') + } + } +} + pub fn (mut g Gen) infix_from_typ(typ ast.Type, op token.Kind) { if g.is_param_type(typ) { - eprintln(*g.table.sym(typ)) - panic('unimplemented') + g.infix_param_type(typ, op) + } else { + g.infix_numeric_type(typ, op) } +} +fn (mut g Gen) infix_numeric_type(typ ast.Type, op token.Kind) { wasm_typ := g.as_numtype(g.get_wasm_type(typ)) + // This adds two tiers of comparaison but is a lot cleaner match op { - .plus { - g.func.add(wasm_typ) - } - .minus { - g.func.sub(wasm_typ) - } - .mul { - g.func.mul(wasm_typ) - } - .mod { - g.func.rem(wasm_typ, typ.is_signed()) - } - .div { - g.func.div(wasm_typ, typ.is_signed()) - } - .eq { - g.func.eq(wasm_typ) - } - .ne { - g.func.ne(wasm_typ) - } - .gt { - g.func.gt(wasm_typ, typ.is_signed()) - } - .lt { - g.func.lt(wasm_typ, typ.is_signed()) - } - .ge { - g.func.ge(wasm_typ, typ.is_signed()) - } - .le { - g.func.le(wasm_typ, typ.is_signed()) + .plus, .minus, .mul, .div, .mod { + g.emit_arithmetic_op(wasm_typ, typ, op) } - .xor { - g.func.b_xor(wasm_typ) + .eq, .ne, .gt, .lt, .ge, .le { + g.emit_comparison_op(wasm_typ, typ, op) } - .pipe { - g.func.b_or(wasm_typ) - } - .amp { - g.func.b_and(wasm_typ) - } - .left_shift { - g.func.b_shl(wasm_typ) - } - .right_shift { - g.func.b_shr(wasm_typ, true) - } - .unsigned_right_shift { - g.func.b_shr(wasm_typ, false) + .xor, .pipe, .amp, .left_shift, .right_shift, .unsigned_right_shift { + g.emit_bitwise_op(wasm_typ, typ, op) } else { g.w_error('bad infix: op `${op}`') } } } + +fn (mut g Gen) emit_arithmetic_op(wasm_typ wasm.NumType, typ ast.Type, op token.Kind) { + match op { + .plus { g.func.add(wasm_typ) } + .minus { g.func.sub(wasm_typ) } + .mul { g.func.mul(wasm_typ) } + .div { g.func.div(wasm_typ, typ.is_signed()) } + .mod { g.func.rem(wasm_typ, typ.is_signed()) } + else { g.w_error('invalid aritmetic op: `${op}`') } + } +} + +fn (mut g Gen) emit_comparison_op(wasm_typ wasm.NumType, typ ast.Type, op token.Kind) { + is_signed := typ.is_signed() + match op { + .eq { g.func.eq(wasm_typ) } + .ne { g.func.ne(wasm_typ) } + .gt { g.func.gt(wasm_typ, is_signed) } + .lt { g.func.lt(wasm_typ, is_signed) } + .ge { g.func.ge(wasm_typ, is_signed) } + .le { g.func.le(wasm_typ, is_signed) } + else { g.w_error('invalid comparison op: `${op}`') } + } +} + +fn (mut g Gen) emit_bitwise_op(wasm_typ wasm.NumType, typ ast.Type, op token.Kind) { + match op { + .xor { g.func.b_xor(wasm_typ) } + .pipe { g.func.b_or(wasm_typ) } + .amp { g.func.b_and(wasm_typ) } + .left_shift { g.func.b_shl(wasm_typ) } + .right_shift { g.func.b_shr(wasm_typ, true) } + .unsigned_right_shift { g.func.b_shr(wasm_typ, false) } + else { g.w_error('invalid bitwise op: `${op}`') } + } +} diff --git a/vlib/v/gen/wasm/tests/builtin.vv b/vlib/v/gen/wasm/tests/builtin.vv index 7a151e12b..5819bff90 100644 --- a/vlib/v/gen/wasm/tests/builtin.vv +++ b/vlib/v/gen/wasm/tests/builtin.vv @@ -3,6 +3,70 @@ fn test() { println('hello!') } +fn str_concat() { + a := 'Hello ' + b := 'V!' + d := a + b + println(d) +} + +fn str_loop_concat() { + mut a := 'Y' + for _ in 0 .. 20 { + a += 'o' + } + a += '!' + println(a) +} + +fn str_cmp() { + a := 'Test???' + b := 'TestingMcTestface' + c := 'TestingMcTestface' + + println(a != b) + println(a == b) + println(a == c) + println(c != b) + println(b == a) + + // Duh + println(b == b) + println(a == a) + println(c == c) + + // Dynamically Generated + mut d := '' + e := 'aaaaaaaa' + for _ in 0 .. 8 { + d += 'a' + } + println(d == e) + println(d != e) + + // Complex expressions + println(d == e && d == e) + println(d == e && d == a) + + println(b < a) + println(b > a) + println(b <= a) + println(b >= a) + + // If eval + if b == c { + println('B is C') + } + if a != c { + println('A is not C') + if a == c { + panic('A is C') + } else { + println('A is not C') + } + } +} + fn str_methods() { print(128.str()) println(i64(-192322).str()) @@ -27,6 +91,9 @@ fn main() { test() str_methods() str_implicit() + str_concat() + str_cmp() + str_loop_concat() assertions() // panic('nooo!') diff --git a/vlib/v/gen/wasm/tests/builtin.vv.out b/vlib/v/gen/wasm/tests/builtin.vv.out index 2fd555326..73288584b 100644 --- a/vlib/v/gen/wasm/tests/builtin.vv.out +++ b/vlib/v/gen/wasm/tests/builtin.vv.out @@ -4,6 +4,27 @@ false false true 110 +Hello V! +true +false +false +false +false +true +true +true +true +false +true +false +false +true +false +true +B is C +A is not C +A is not C +Yoooooooooooooooooooo! wasm builtins 1 1 \ No newline at end of file diff --git a/vlib/v/gen/wasm/tests/match.vv b/vlib/v/gen/wasm/tests/match.vv index d63aa1387..e9a41f92d 100644 --- a/vlib/v/gen/wasm/tests/match.vv +++ b/vlib/v/gen/wasm/tests/match.vv @@ -94,15 +94,11 @@ fn main() { println('Function calling matches -------') function_matches(42) - /* - println("Strings matches -------") - - // This doesn't work yet because string comparison is not ready ( + println('Strings matches -------') h := 'Hello' println(match h { 'NotHello' { 'Not Hello string' } 'Hello' { 'Hello string' } else { 'Unknown' } }) - */ } diff --git a/vlib/v/gen/wasm/tests/match.vv.out b/vlib/v/gen/wasm/tests/match.vv.out index f95430f8e..5ee061357 100644 --- a/vlib/v/gen/wasm/tests/match.vv.out +++ b/vlib/v/gen/wasm/tests/match.vv.out @@ -10,4 +10,6 @@ Bool matches ------- OK OK Function calling matches ------- -function_matches: OK \ No newline at end of file +function_matches: OK +Strings matches ------- +Hello string \ No newline at end of file -- 2.39.5