veb: deprecate x.vweb in favor of veb; checker: show missing variants in the sumtype error

This commit is contained in:
Alexander Medvednikov 2024-08-18 17:27:08 +03:00
parent ceac4baf87
commit ae1b9ed571
29 changed files with 176 additions and 63 deletions

View File

@ -1,5 +1,5 @@
import time
import x.vweb
import veb
// See https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS
// and https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#preflighted_requests
@ -7,24 +7,26 @@ import x.vweb
// > a server to indicate any origins (domain, scheme, or port) other than its own from
// > which a browser should permit loading resources...
// Usage: do `./v run examples/xvweb/cors/` to start the app,
// Usage: do `./v run examples/xveb/cors/` to start the app,
// then check the headers in another shell:
//
// 1) `curl -vvv -X OPTIONS http://localhost:45678/time`
// 2) `curl -vvv -X POST http://localhost:45678/time`
pub struct Context {
vweb.Context
veb.Context
}
pub struct App {
vweb.Middleware[Context]
veb.Middleware[Context]
}
// time is a simple POST request handler, that returns the current time. It should be available
// to JS scripts, running on arbitrary other origins/domains.
// pub fn (app &App) time() veb.Result {
@[post]
pub fn (app &App) time(mut ctx Context) vweb.Result {
pub fn (app &App) time(mut ctx Context) veb.Result {
return ctx.json({
'time': time.now().format_ss_milli()
})
@ -32,9 +34,8 @@ pub fn (app &App) time(mut ctx Context) vweb.Result {
fn main() {
println("
To test, if CORS works, copy this JS snippet, then go to for example https://stackoverflow.com/ ,
press F12, then paste the snippet in the opened JS console. You should see the vweb server's time:
To test, if CORS works, copy this JS snippet, then go to for example https://stackoverflow.com/ ,
press F12, then paste the snippet in the opened JS console. You should see the veb server's time:
var xhr = new XMLHttpRequest();
xhr.onload = function(data) {
console.log('xhr loaded');
@ -46,13 +47,13 @@ xhr.send();
mut app := &App{}
// use vweb's cors middleware to handle CORS requests
app.use(vweb.cors[Context](vweb.CorsOptions{
// use veb's cors middleware to handle CORS requests
app.use(veb.cors[Context](veb.CorsOptions{
// allow CORS requests from every domain
origins: ['*']
// allow CORS requests with the following request methods:
allowed_methods: [.get, .head, .patch, .put, .post, .delete]
}))
vweb.run[App, Context](mut app, 45678)
veb.run[App, Context](mut app, 45678)
}

View File

@ -1,10 +1,10 @@
// Simple TODO app using x.vweb
// Simple TODO app using veb
// Run from this directory with `v run main.v`
// You can also enable vwebs livereload feature with
// `v watch -d vweb_livereload run main.v`
// You can also enable vebs livereload feature with
// `v watch -d veb_livereload run main.v`
module main
import x.vweb
import veb
import db.sqlite
import os
import time
@ -21,14 +21,14 @@ pub mut:
}
pub struct Context {
vweb.Context
veb.Context
pub mut:
// we can use this field to check whether we just created a TODO in our html templates
created_todo bool
}
pub struct App {
vweb.StaticHandler
veb.StaticHandler
pub:
// we can access the SQLITE database directly via `app.db`
db sqlite.DB
@ -36,16 +36,16 @@ pub:
// This method will only handle GET requests to the index page
@[get]
pub fn (app &App) index(mut ctx Context) vweb.Result {
pub fn (app &App) index(mut ctx Context) veb.Result {
todos := sql app.db {
select from Todo
} or { return ctx.server_error('could not fetch todos from database!') }
return $vweb.html()
return $veb.html()
}
// This method will only handle POST requests to the index page
@['/'; post]
pub fn (app &App) create_todo(mut ctx Context, name string) vweb.Result {
pub fn (app &App) create_todo(mut ctx Context, name string) veb.Result {
// We can receive form input fields as arguments in a route!
// we could also access the name field by doing `name := ctx.form['name']`
@ -78,7 +78,7 @@ pub fn (app &App) create_todo(mut ctx Context, name string) vweb.Result {
}
@['/todo/:id/complete'; post]
pub fn (app &App) complete_todo(mut ctx Context, id int) vweb.Result {
pub fn (app &App) complete_todo(mut ctx Context, id int) veb.Result {
// first check if there exist a TODO record with `id`
todos := sql app.db {
select from Todo where id == id
@ -99,7 +99,7 @@ pub fn (app &App) complete_todo(mut ctx Context, id int) vweb.Result {
}
@['/todo/:id/delete'; post]
pub fn (app &App) delete_todo(mut ctx Context, id int) vweb.Result {
pub fn (app &App) delete_todo(mut ctx Context, id int) veb.Result {
// first check if there exist a TODO record with `id`
todos := sql app.db {
select from Todo where id == id
@ -141,5 +141,5 @@ fn main() {
}!
// start our app at port 8080
vweb.run[App, Context](mut app, 8080)
veb.run[App, Context](mut app, 8080)
}

View File

@ -1,14 +1,14 @@
module main
import x.vweb
import veb
import os
pub struct Context {
vweb.Context
veb.Context
}
pub struct App {
vweb.StaticHandler
veb.StaticHandler
}
fn main() {
@ -17,5 +17,5 @@ fn main() {
os.chdir(os.dir(os.executable()))!
mut app := &App{}
app.handle_static('dist', true)!
vweb.run[App, Context](mut app, 8080)
veb.run[App, Context](mut app, 8080)
}

View File

@ -27,6 +27,7 @@ pub fn (app &App) before_request() {
@['/users/:user']
pub fn (mut app App) user_endpoint(mut ctx Context, user string) veb.Result {
// pub fn (mut app App) user_endpoint(user string) veb.Result {
id := rand.intn(100) or { 0 }
return ctx.json({
user: id

View File

@ -940,6 +940,14 @@ pub fn (s string) rsplit_once(delim string) ?(string, string) {
return result[1], result[0]
}
// split_n splits the string based on the passed `delim` substring.
// It returns the first Nth parts. When N=0, return all the splits.
// The last returned element has the remainder of the string, even if
// the remainder contains more `delim` substrings.
pub fn (s string) split_n(delim string, n int) []string {
return s.split_nth(delim, n)
}
// split_nth splits the string based on the passed `delim` substring.
// It returns the first Nth parts. When N=0, return all the splits.
// The last returned element has the remainder of the string, even if

View File

@ -533,12 +533,11 @@ pub fn (t &Table) find_field(s &TypeSymbol, name string) !StructField {
}
SumType {
t.resolve_common_sumtype_fields(mut ts)
if field := ts.info.find_field(name) {
if field := ts.info.find_sum_type_field(name) {
return field
}
// mut info := ts.info as SumType
// TODO: a more detailed error so that it's easier to fix?
return error('field `${name}` does not exist or have the same type in all sumtype variants')
missing_variants := t.find_missing_variants(ts.info, name)
return error('field `${name}` does not exist or have the same type in these sumtype `${ts.name}` variants: ${missing_variants}')
}
else {}
}

View File

@ -1748,7 +1748,7 @@ pub fn (t &TypeSymbol) find_field(name string) ?StructField {
Aggregate { return t.info.find_field(name) }
Struct { return t.info.find_field(name) }
Interface { return t.info.find_field(name) }
SumType { return t.info.find_field(name) }
SumType { return t.info.find_sum_type_field(name) }
else { return none }
}
}
@ -1811,7 +1811,7 @@ pub fn (s Struct) get_field(name string) StructField {
panic('unknown field `${name}`')
}
pub fn (s &SumType) find_field(name string) ?StructField {
pub fn (s &SumType) find_sum_type_field(name string) ?StructField {
for mut field in unsafe { s.fields } {
if field.name == name {
return field
@ -1820,6 +1820,34 @@ pub fn (s &SumType) find_field(name string) ?StructField {
return none
}
// For the 'field does not exist or have the same type in all sumtype variants' error.
// To print all sumtype variants the developer has to fix.
pub fn (t &Table) find_missing_variants(s &SumType, field_name string) string {
mut res := []string{cap: 5}
for variant in s.variants {
ts := t.sym(variant)
if ts.kind != .struct_ {
continue
}
mut found := false
struct_info := ts.info as Struct
for field in struct_info.fields {
if field.name == field_name {
found = true
break
}
}
if !found {
res << ts.name
}
}
// println('!!!!! field_name=${field_name}')
// print_backtrace()
// println(res)
str := res.join(', ')
return str.replace("'", '`')
}
pub fn (i Interface) defines_method(name string) bool {
if i.methods.any(it.name == name) {
return true

View File

@ -953,7 +953,7 @@ fn (mut c Checker) fail_if_immutable(mut expr ast.Expr) (string, token.Pos) {
}
.sum_type {
sumtype_info := typ_sym.info as ast.SumType
mut field_info := sumtype_info.find_field(expr.field_name) or {
mut field_info := sumtype_info.find_sum_type_field(expr.field_name) or {
type_str := c.table.type_to_str(expr.expr_type)
c.error('unknown field `${type_str}.${expr.field_name}`', expr.pos)
return '', expr.pos
@ -1596,6 +1596,12 @@ fn (mut c Checker) selector_expr(mut node ast.SelectorExpr) ast.Type {
node.from_embed_types = embed_types
if sym.kind in [.aggregate, .sum_type] {
unknown_field_msg = err.msg()
// TODO need a better way to check that we need to display sum type variants info
if unknown_field_msg.contains('does not exist or have the same type in all sumtype') {
info := sym.info as ast.SumType
missing_variants := c.table.find_missing_variants(info, field_name)
unknown_field_msg += missing_variants
}
}
}
if !c.inside_unsafe {
@ -2598,6 +2604,9 @@ fn (mut c Checker) hash_stmt(mut node ast.HashStmt) {
}
fn (mut c Checker) import_stmt(node ast.Import) {
if node.mod == 'x.vweb' {
println('`x.vweb` is now `veb`. The module is no longer experimental. Simply `import veb` instead of `import x.vweb`.')
}
c.check_valid_snake_case(node.alias, 'module alias', node.pos)
for sym in node.syms {
name := '${node.mod}.${sym.name}'

View File

@ -215,6 +215,7 @@ fn (mut c Checker) deprecate(kind string, name string, attrs []ast.Attr, pos tok
c.warn(semicolonize('${start_message} has been deprecated since ${after_time.ymmdd()}, it will be an error after ${error_time.ymmdd()}',
deprecation_message), pos)
} else if after_time == now {
// print_backtrace()
c.warn(semicolonize('${start_message} has been deprecated', deprecation_message),
pos)
// c.warn(semicolonize('${start_message} has been deprecated!11 m=${deprecation_message}',

View File

@ -433,6 +433,36 @@ fn (mut c Checker) fn_decl(mut node ast.FnDecl) {
}
}
c.fn_scope = node.scope
// Register implicit context var
typ_veb_result := c.table.find_type_idx('veb.Result')
if node.return_type == typ_veb_result {
typ_veb_context := c.table.find_type_idx('veb.Context')
// No `ctx` param? Add it
if !node.params.any(it.name == 'ctx') && node.params.len > 1 {
params := node.params.clone()
ctx_param := ast.Param{
name: 'ctx'
typ: typ_veb_context
is_mut: true
}
node.params = [node.params[0], ctx_param]
node.params << params[1..]
println('new params ${node.name}')
// println(node.params)
}
// sym := c.table.sym(typ_veb_context)
// println('reging ${typ_veb_context} ${sym}')
// println(c.fn_scope)
// println(node.params)
c.fn_scope.register(ast.Var{
name: 'ctx'
typ: typ_veb_context
pos: node.pos
is_used: true
is_mut: true
is_stack_obj: false // true
})
}
c.stmts(mut node.stmts)
node_has_top_return := has_top_return(node.stmts)
node.has_return = c.returns || node_has_top_return
@ -1058,6 +1088,7 @@ fn (mut c Checker) fn_call(mut node ast.CallExpr, mut continue_check &bool) ast.
}
}
// XTODO document
if typ != 0 {
generic_vts := c.table.final_sym(typ)
if generic_vts.info is ast.FnType {
@ -1902,7 +1933,7 @@ fn (mut c Checker) method_call(mut node ast.CallExpr) ast.Type {
}
left_type := c.expr(mut node.left)
if left_type == ast.void_type {
c.error('cannot call a method using an invalid expression', node.pos)
// c.error('cannot call a method using an invalid expression', node.pos)
return ast.void_type
}
c.expected_type = left_type
@ -2726,7 +2757,8 @@ fn (mut c Checker) post_process_generic_fns() ! {
for concrete_types in gtypes {
c.table.cur_concrete_types = concrete_types
c.fn_decl(mut node)
if node.name in ['x.vweb.run', 'x.vweb.run_at', 'vweb.run', 'vweb.run_at'] {
if node.name in ['veb.run', 'veb.run_at', 'x.vweb.run', 'x.vweb.run_at', 'vweb.run',
'vweb.run_at'] {
for ct in concrete_types {
if ct !in c.vweb_gen_types {
c.vweb_gen_types << ct

View File

@ -67,6 +67,7 @@ fn (mut c Checker) if_expr(mut node ast.IfExpr) ast.Type {
cond_typ := c.table.unaliased_type(c.unwrap_generic(c.expr(mut branch.cond)))
if (cond_typ.idx() != ast.bool_type_idx || cond_typ.has_flag(.option)
|| cond_typ.has_flag(.result)) && !c.pref.translated && !c.file.is_translated {
//&& cond_typ.idx() != ast.void_type_idx { TODO bring back after the void split
c.error('non-bool type `${c.table.type_to_str(cond_typ)}` used as if condition',
branch.cond.pos())
}

View File

@ -799,9 +799,12 @@ fn (mut c Checker) infix_expr(mut node ast.InfixExpr) ast.Type {
}
.and, .logical_or {
if !c.pref.translated && !c.file.is_translated {
// TODO Bring back once I split void into void and bad
// if left_final_sym.kind !in [.bool, .void] {
if left_final_sym.kind != .bool {
c.error('left operand for `${node.op}` is not a boolean', node.left.pos())
}
// if right_final_sym.kind !in [.bool, .void] {
if right_final_sym.kind != .bool {
c.error('right operand for `${node.op}` is not a boolean', node.right.pos())
}

View File

@ -5,7 +5,7 @@ vlib/v/checker/tests/incorrect_smartcast2_err.vv:24:9: notice: smartcast can onl
| ~~~
25 | Left[int] {
26 | println(v[0].error)
vlib/v/checker/tests/incorrect_smartcast2_err.vv:26:17: error: field `error` does not exist or have the same type in all sumtype variants
vlib/v/checker/tests/incorrect_smartcast2_err.vv:26:17: error: field `error` does not exist or have the same type in these sumtype `Either[int, int]` variants: Right[int]
24 | match v[0] {
25 | Left[int] {
26 | println(v[0].error)

View File

@ -1,4 +1,4 @@
vlib/v/checker/tests/sum_type_common_fields_alias_error.vv:35:14: error: field `name` does not exist or have the same type in all sumtype variants
vlib/v/checker/tests/sum_type_common_fields_alias_error.vv:35:14: error: field `name` does not exist or have the same type in these sumtype `Main` variants:
33 | }
34 | println(m)
35 | assert m[0].name == 'abc'
@ -12,7 +12,7 @@ vlib/v/checker/tests/sum_type_common_fields_alias_error.vv:35:9: error: assert c
| ~~~~~~~~~~~~~~~~~~
36 | assert m[1].name == 'def'
37 | assert m[2].name == 'xyz'
vlib/v/checker/tests/sum_type_common_fields_alias_error.vv:36:14: error: field `name` does not exist or have the same type in all sumtype variants
vlib/v/checker/tests/sum_type_common_fields_alias_error.vv:36:14: error: field `name` does not exist or have the same type in these sumtype `Main` variants:
34 | println(m)
35 | assert m[0].name == 'abc'
36 | assert m[1].name == 'def'
@ -26,7 +26,7 @@ vlib/v/checker/tests/sum_type_common_fields_alias_error.vv:36:9: error: assert c
| ~~~~~~~~~~~~~~~~~~
37 | assert m[2].name == 'xyz'
38 | }
vlib/v/checker/tests/sum_type_common_fields_alias_error.vv:37:14: error: field `name` does not exist or have the same type in all sumtype variants
vlib/v/checker/tests/sum_type_common_fields_alias_error.vv:37:14: error: field `name` does not exist or have the same type in these sumtype `Main` variants:
35 | assert m[0].name == 'abc'
36 | assert m[1].name == 'def'
37 | assert m[2].name == 'xyz'

View File

@ -1,4 +1,4 @@
vlib/v/checker/tests/sum_type_common_fields_error.vv:53:14: error: field `val` does not exist or have the same type in all sumtype variants
vlib/v/checker/tests/sum_type_common_fields_error.vv:53:14: error: field `val` does not exist or have the same type in these sumtype `Main` variants:
51 | assert m[2].name == '64bit integer'
52 | assert m[3].name == 'string'
53 | assert m[0].val == 123

View File

@ -1,5 +1,5 @@
vlib/v/checker/tests/void_method_call.vv:5:17: error: cannot call a method using an invalid expression
3 |
vlib/v/checker/tests/void_method_call.vv:5:17: cgen error: checker bug; CallExpr.left_type is 0 in method_call
3 |
4 | fn main() {
5 | simple_fn().method()
| ~~~~~~~~

View File

@ -109,7 +109,7 @@ pub fn (f &Fmt) type_to_str_using_aliases(typ ast.Type, import_aliases map[strin
println('${s}')
}
if s.starts_with('x.vweb') {
s = s.replace_once('x.vweb', 'veb.')
s = s.replace_once('x.vweb.', 'veb.')
}
return s
}

View File

@ -168,11 +168,11 @@ fn (mut g Gen) comptime_call(mut node ast.ComptimeCall) {
} else {
if !has_decompose {
// do not generate anything if the argument lengths don't match
g.writeln('/* skipping ${sym.name}.${m.name} due to mismatched arguments list */')
g.writeln('/* skipping ${sym.name}.${m.name} due to mismatched arguments list: node.args=${node.args.len} m.params=${m.params.len} */')
// g.writeln('println(_SLIT("skipping ${node.sym.name}.$m.name due to mismatched arguments list"));')
// eprintln('info: skipping ${node.sym.name}.$m.name due to mismatched arguments list\n' +
//'method.params: $m.params, args: $node.args\n\n')
// verror('expected ${m.params.len-1} arguments to method ${node.sym.name}.$m.name, but got $node.args.len')
// verror('expected ${m.params.len - 1} arguments to method ${node.sym.name}.${m.name}, but got ${node.args.len}')
return
}
}

View File

@ -704,6 +704,20 @@ fn (mut g Gen) fn_decl_params(params []ast.Param, scope &ast.Scope, is_variadic
// in C, `()` is untyped, unlike `(void)`
g.write('void')
}
/// mut is_implicit_ctx := false
// Veb actions defined by user can have implicit context
/*
if g.cur_fn != unsafe { nil } && g.cur_fn.is_method && g.cur_mod.name != 'veb' {
typ_veb_result := g.table.find_type_idx('veb.Result')
// if params.len == 3 {
// println(g.cur_fn)
//}
if g.cur_fn.return_type == typ_veb_result {
// is_implicit_ctx = true
g.write('/*veb*/')
}
}
*/
for i, param in params {
mut caname := if param.name == '_' {
g.new_tmp_declaration_name()
@ -756,6 +770,11 @@ fn (mut g Gen) fn_decl_params(params []ast.Param, scope &ast.Scope, is_variadic
g.write(', ')
g.definitions.write_string(', ')
}
// if is_implicit_ctx && i == 0 && params[1].name != 'ctx' {
// g.writeln('veb__Context* ctx,')
// g.definitions.write_string('veb__Context* ctx,')
//}
}
if (g.pref.translated && is_variadic) || is_c_variadic {
g.write(', ... ')

View File

@ -306,6 +306,7 @@ pub fn mark_used(mut table ast.Table, mut pref_ pref.Preferences, ast_files []&a
handle_vweb(mut table, mut all_fn_root_names, 'vweb.Result', 'vweb.filter', 'vweb.Context')
handle_vweb(mut table, mut all_fn_root_names, 'x.vweb.Result', 'x.vweb.filter', 'x.vweb.Context')
handle_vweb(mut table, mut all_fn_root_names, 'veb.Result', 'veb.filter', 'veb.Context')
// handle ORM drivers:
orm_connection_implementations := table.iface_types['orm.Connection'] or { []ast.Type{} }

View File

@ -591,6 +591,16 @@ run them via `v file.v` instead',
language: language
})
}
/*
// Register implicit context var
p.scope.register(ast.Var{
name: 'ctx'
typ: ast.error_type
pos: p.tok.pos()
is_used: true
is_stack_obj: true
})
*/
// Body
p.cur_fn_name = name
mut stmts := []ast.Stmt{}

View File

@ -1,10 +1,10 @@
import x.vweb
import veb
import time
pub struct App {}
pub struct Context {
vweb.Context
veb.Context
}
fn main() {
@ -15,10 +15,10 @@ fn main() {
}()
time.sleep(10 * time.second)
mut app := &App{}
vweb.run_at[App, Context](mut app, port: 38090)!
veb.run_at[App, Context](mut app, port: 38090)!
}
@['/']
pub fn (app &App) index(mut ctx Context) vweb.Result {
pub fn (app &App) index(mut ctx Context) veb.Result {
return ctx.text('Hello World')
}

View File

@ -1,7 +1,7 @@
import net.http
import time
import x.sessions
import x.vweb
import veb
import x.sessions.vweb2_middleware
const port = 13010
@ -24,12 +24,12 @@ const default_user = User{
}
pub struct Context {
vweb.Context
veb.Context
sessions.CurrentSession[User]
}
pub struct App {
vweb.Middleware[Context]
veb.Middleware[Context]
pub mut:
sessions &sessions.Sessions[User]
started chan bool
@ -39,11 +39,11 @@ pub fn (mut app App) before_accept_loop() {
app.started <- true
}
pub fn (app &App) session_data(mut ctx Context) vweb.Result {
pub fn (app &App) session_data(mut ctx Context) veb.Result {
return ctx.text(ctx.session_data.str())
}
pub fn (app &App) protected(mut ctx Context) vweb.Result {
pub fn (app &App) protected(mut ctx Context) veb.Result {
if user := ctx.session_data {
return ctx.json(user)
} else {
@ -52,12 +52,12 @@ pub fn (app &App) protected(mut ctx Context) vweb.Result {
}
}
pub fn (mut app App) save_session(mut ctx Context) vweb.Result {
pub fn (mut app App) save_session(mut ctx Context) veb.Result {
app.sessions.save(mut ctx, default_user) or { return ctx.server_error(err.msg()) }
return ctx.ok('')
}
pub fn (mut app App) update_session(mut ctx Context) vweb.Result {
pub fn (mut app App) update_session(mut ctx Context) veb.Result {
if mut user := ctx.session_data {
user.age++
app.sessions.save(mut ctx, user) or { return ctx.server_error(err.msg()) }
@ -72,7 +72,7 @@ pub fn (mut app App) update_session(mut ctx Context) vweb.Result {
return ctx.ok('')
}
pub fn (mut app App) destroy_session(mut ctx Context) vweb.Result {
pub fn (mut app App) destroy_session(mut ctx Context) veb.Result {
app.sessions.destroy(mut ctx) or { return ctx.server_error(err.msg()) }
// sessions module should also update the context
assert ctx.session_data == none
@ -100,7 +100,7 @@ fn testsuite_begin() {
app.use(vweb2_middleware.create[User, Context](mut app.sessions))
spawn vweb.run_at[App, Context](mut app, port: port, timeout_in_seconds: 2)
spawn veb.run_at[App, Context](mut app, port: port, timeout_in_seconds: 2)
// app startup time
_ := <-app.started
}

View File

@ -1,15 +1,15 @@
module vweb2_middleware
import x.sessions
import x.vweb
import veb
// middleware can be used to add session middleware to your vweb app to ensure
// a valid session always exists. If a valid session exists the session data will
// be loaded into `session_data`, else a new session id will be generated.
// You have to pass the Context type as the generic type
// Example: app.use(app.sessions.middleware[Context]())
pub fn create[T, X](mut s sessions.Sessions[T]) vweb.MiddlewareOptions[X] {
return vweb.MiddlewareOptions[X]{
pub fn create[T, X](mut s sessions.Sessions[T]) veb.MiddlewareOptions[X] {
return veb.MiddlewareOptions[X]{
handler: fn [mut s] [T, X](mut ctx X) bool {
// a session id is retrieved from the client, so it must be considered
// untrusted and has to be verified on every request

View File

@ -1,4 +1,4 @@
//@[deprecated: '`x.vweb` is now `veb`. The module is no longer experimental.']
@[deprecated: '`x.vweb` is now `veb`. The module is no longer experimental. Simply import veb instead of x.vweb']
module vweb
import io