1
0
mirror of https://github.com/vlang/v.git synced 2023-08-10 21:13:21 +03:00
v/vlib/v/parser/comptime.v

423 lines
10 KiB
V

// Copyright (c) 2019-2023 Alexander Medvednikov. All rights reserved.
// Use of this source code is governed by an MIT license
// that can be found in the LICENSE file.
module parser
import os
import v.ast
import v.pref
import v.token
const (
supported_comptime_calls = ['html', 'tmpl', 'env', 'embed_file', 'pkgconfig', 'compile_error',
'compile_warn', 'res']
comptime_types = ['map', 'array', 'int', 'float', 'struct', 'interface', 'enum',
'sumtype', 'alias', 'function', 'option']
)
fn (mut p Parser) parse_comptime_type() ast.ComptimeType {
mut node := ast.ComptimeType{ast.ComptimeTypeKind.map_, p.tok.pos()}
p.check(.dollar)
name := p.check_name()
if name !in parser.comptime_types {
p.error('unsupported compile-time type `${name}`: only ${parser.comptime_types} are supported')
}
mut cty := ast.ComptimeTypeKind.map_
match name {
'map' {
cty = .map_
}
'struct' {
cty = .struct_
}
'interface' {
cty = .iface
}
'int' {
cty = .int
}
'float' {
cty = .float
}
'alias' {
cty = .alias
}
'function' {
cty = .function
}
'array' {
cty = .array
}
'enum' {
cty = .enum_
}
'sumtype' {
cty = .sum_type
}
'option' {
cty = .option
}
else {}
}
node = ast.ComptimeType{cty, node.pos}
return node
}
// // #include, #flag, #v
fn (mut p Parser) hash() ast.HashStmt {
pos := p.tok.pos()
val := p.tok.lit
kind := val.all_before(' ')
p.next()
mut main_str := ''
mut msg := ''
content := val.all_after('${kind} ').all_before('//')
if content.contains(' #') {
main_str = content.all_before(' #').trim_space()
msg = content.all_after(' #').trim_space()
} else {
main_str = content.trim_space()
msg = ''
}
return ast.HashStmt{
mod: p.mod
source_file: p.file_name
val: val
kind: kind
main: main_str
msg: msg
pos: pos
}
}
fn (mut p Parser) comptime_call() ast.ComptimeCall {
err_node := ast.ComptimeCall{
scope: 0
}
start_pos := p.tok.pos()
p.check(.dollar)
error_msg := 'only `\$tmpl()`, `\$env()`, `\$embed_file()`, `\$pkgconfig()`, `\$vweb.html()`, `\$compile_error()`, `\$compile_warn()` and `\$res()` comptime functions are supported right now'
if p.peek_tok.kind == .dot {
name := p.check_name() // skip `vweb.html()` TODO
if name != 'vweb' {
p.error(error_msg)
return err_node
}
p.check(.dot)
}
method_name := p.check_name()
if method_name !in parser.supported_comptime_calls {
p.error(error_msg)
return err_node
}
is_embed_file := method_name == 'embed_file'
is_html := method_name == 'html'
// $env('ENV_VAR_NAME')
p.check(.lpar)
arg_pos := p.tok.pos()
if method_name in ['env', 'pkgconfig', 'compile_error', 'compile_warn'] {
s := p.tok.lit
p.check(.string)
p.check(.rpar)
return ast.ComptimeCall{
scope: 0
method_name: method_name
args_var: s
is_env: method_name == 'env'
is_pkgconfig: method_name == 'pkgconfig'
env_pos: start_pos
pos: start_pos.extend(p.prev_tok.pos())
}
} else if method_name == 'res' {
mut has_args := false
mut type_index := ''
if p.tok.kind == .number {
has_args = true
type_index = p.tok.lit
p.check(.number)
}
p.check(.rpar)
if has_args {
return ast.ComptimeCall{
scope: 0
method_name: method_name
args_var: type_index
pos: start_pos.extend(p.prev_tok.pos())
}
}
return ast.ComptimeCall{
scope: 0
method_name: method_name
pos: start_pos.extend(p.prev_tok.pos())
}
}
mut literal_string_param := if is_html { '' } else { p.tok.lit }
if p.tok.kind == .name {
if var := p.scope.find_var(p.tok.lit) {
if var.expr is ast.StringLiteral {
literal_string_param = var.expr.val
}
} else if var := p.table.global_scope.find_const(p.mod + '.' + p.tok.lit) {
if var.expr is ast.StringLiteral {
literal_string_param = var.expr.val
}
}
}
path_of_literal_string_param := literal_string_param.replace('/', os.path_separator)
mut arg := ast.CallArg{}
if !is_html {
arg_expr := p.expr(0)
arg = ast.CallArg{
expr: arg_expr
}
}
mut embed_compression_type := 'none'
if is_embed_file {
if p.tok.kind == .comma {
p.next()
p.check(.dot)
embed_compression_type = p.check_name()
}
}
p.check(.rpar)
// $embed_file('/path/to/file')
if is_embed_file {
p.register_auto_import('v.preludes.embed_file')
if embed_compression_type == 'zlib' {
p.register_auto_import('v.preludes.embed_file.zlib')
}
return ast.ComptimeCall{
scope: 0
is_embed: true
embed_file: ast.EmbeddedFile{
compression_type: embed_compression_type
}
args: [arg]
pos: start_pos.extend(p.prev_tok.pos())
}
}
// Compile vweb html template to V code, parse that V code and embed the resulting V function
// that returns an html string.
fn_path := p.cur_fn_name.split('_')
fn_path_joined := fn_path.join(os.path_separator)
compiled_vfile_path := os.real_path(p.scanner.file_path.replace('/', os.path_separator))
tmpl_path := if is_html { '${fn_path.last()}.html' } else { path_of_literal_string_param }
// Looking next to the vweb program
dir := os.dir(compiled_vfile_path)
mut path := os.join_path_single(dir, fn_path_joined)
path += '.html'
path = os.real_path(path)
if !is_html {
if os.is_abs_path(tmpl_path) {
path = tmpl_path
} else {
path = os.join_path_single(dir, tmpl_path)
}
}
if !os.exists(path) {
if is_html {
// can be in `templates/`
path = os.join_path(dir, 'templates', fn_path_joined)
path += '.html'
}
if !os.exists(path) {
if p.pref.is_fmt {
return ast.ComptimeCall{
scope: 0
is_vweb: true
method_name: method_name
args_var: literal_string_param
args: [arg]
pos: start_pos.extend(p.prev_tok.pos())
}
}
if is_html {
p.error_with_pos('vweb HTML template "${tmpl_path}" not found', arg_pos)
} else {
p.error_with_pos('template file "${tmpl_path}" not found', arg_pos)
}
return err_node
}
// println('path is now "$path"')
}
tmp_fn_name := p.cur_fn_name.replace('.', '__') + start_pos.pos.str()
$if trace_comptime ? {
println('>>> compiling comptime template file "${path}" for ${tmp_fn_name}')
}
v_code := p.compile_template_file(path, tmp_fn_name)
$if print_vweb_template_expansions ? {
lines := v_code.split('\n')
for i, line in lines {
println('${path}:${i + 1}: ${line}')
}
}
$if trace_comptime ? {
println('')
println('>>> template for ${path}:')
println(v_code)
println('>>> end of template END')
println('')
}
// the tmpl inherits all parent scopes. previous functionality was just to
// inherit the scope from which the comptime call was made and no parents.
// this is much simpler and allows access to globals. can be changed if needed.
mut file := parse_comptime(tmpl_path, v_code, p.table, p.pref, p.scope)
file.path = tmpl_path
return ast.ComptimeCall{
scope: 0
is_vweb: true
vweb_tmpl: file
method_name: method_name
args_var: literal_string_param
args: [arg]
pos: start_pos.extend(p.prev_tok.pos())
}
}
fn (mut p Parser) comptime_for() ast.ComptimeFor {
// p.comptime_for() handles these special forms:
// `$for method in App.methods {`
// `$for val in App.values {`
// `$for field in App.fields {`
// `$for attr in App.attributes {`
p.next()
p.check(.key_for)
var_pos := p.tok.pos()
val_var := p.check_name()
p.check(.key_in)
mut typ_pos := p.tok.pos()
lang := p.parse_language()
typ := p.parse_any_type(lang, false, false, false)
typ_pos = typ_pos.extend(p.prev_tok.pos())
p.check(.dot)
for_val := p.check_name()
mut kind := ast.ComptimeForKind.methods
p.open_scope()
if for_val == 'methods' {
p.scope.register(ast.Var{
name: val_var
typ: p.table.find_type_idx('FunctionData')
pos: var_pos
})
} else if for_val == 'values' {
p.scope.register(ast.Var{
name: val_var
typ: p.table.find_type_idx('EnumData')
pos: var_pos
})
kind = .values
} else if for_val == 'fields' {
p.scope.register(ast.Var{
name: val_var
typ: p.table.find_type_idx('FieldData')
pos: var_pos
})
kind = .fields
} else if for_val == 'attributes' {
p.scope.register(ast.Var{
name: val_var
typ: p.table.find_type_idx('StructAttribute')
pos: var_pos
})
kind = .attributes
} else {
p.error_with_pos('unknown kind `${for_val}`, available are: `methods`, `fields`, `values`, or `attributes`',
p.prev_tok.pos())
return ast.ComptimeFor{}
}
spos := p.tok.pos()
stmts := p.parse_block()
p.close_scope()
return ast.ComptimeFor{
val_var: val_var
stmts: stmts
kind: kind
typ: typ
typ_pos: typ_pos
pos: spos.extend(p.tok.pos())
}
}
// @FN, @STRUCT, @MOD etc. See full list in token.valid_at_tokens
fn (mut p Parser) at() ast.AtExpr {
name := p.tok.lit
kind := match name {
'@FN' { token.AtKind.fn_name }
'@METHOD' { token.AtKind.method_name }
'@MOD' { token.AtKind.mod_name }
'@STRUCT' { token.AtKind.struct_name }
'@FILE' { token.AtKind.file_path }
'@LINE' { token.AtKind.line_nr }
'@FILE_LINE' { token.AtKind.file_path_line_nr }
'@COLUMN' { token.AtKind.column_nr }
'@VHASH' { token.AtKind.vhash }
'@VMOD_FILE' { token.AtKind.vmod_file }
'@VEXE' { token.AtKind.vexe_path }
'@VEXEROOT' { token.AtKind.vexeroot_path }
'@VMODROOT' { token.AtKind.vmodroot_path }
'@VROOT' { token.AtKind.vroot_path } // deprecated, use @VEXEROOT or @VMODROOT
else { token.AtKind.unknown }
}
expr := ast.AtExpr{
name: name
pos: p.tok.pos()
kind: kind
}
p.next()
return expr
}
fn (mut p Parser) comptime_selector(left ast.Expr) ast.Expr {
p.check(.dollar)
start_pos := p.prev_tok.pos()
if p.peek_tok.kind == .lpar {
method_pos := p.tok.pos()
method_name := p.check_name()
p.mark_var_as_used(method_name)
// `app.$action()` (`action` is a string)
p.check(.lpar)
args := p.call_args()
p.check(.rpar)
mut or_kind := ast.OrKind.absent
mut or_pos := p.tok.pos()
mut or_stmts := []ast.Stmt{}
if p.tok.kind == .key_orelse {
// `$method() or {}``
or_kind = .block
or_stmts, or_pos = p.or_block(.with_err_var)
}
return ast.ComptimeCall{
left: left
method_name: method_name
method_pos: method_pos
scope: p.scope
args_var: ''
args: args
pos: start_pos.extend(p.prev_tok.pos())
or_block: ast.OrExpr{
stmts: or_stmts
kind: or_kind
pos: or_pos
}
}
}
mut has_parens := false
if p.tok.kind == .lpar {
p.next()
has_parens = true
} else {
p.warn_with_pos('use brackets instead e.g. `s.$(field.name)` - run vfmt', p.tok.pos())
}
expr := p.expr(0)
if has_parens {
p.check(.rpar)
}
return ast.ComptimeSelector{
has_parens: has_parens
left: left
field_expr: expr
pos: start_pos.extend(p.prev_tok.pos())
}
}