| 1 | // Copyright (c) 2019-2024 Alexander Medvednikov. All rights reserved. |
| 2 | // Use of this source code is governed by an MIT license |
| 3 | // that can be found in the LICENSE file. |
| 4 | module parser |
| 5 | |
| 6 | import os |
| 7 | import v.ast |
| 8 | import v.token |
| 9 | import v.errors |
| 10 | import v.util |
| 11 | import v.vmod |
| 12 | |
| 13 | const supported_comptime_calls = ['html', 'tmpl', 'env', 'embed_file', 'pkgconfig', 'compile_error', |
| 14 | 'compile_warn', 'd', 'res', 'zero', 'new'] |
| 15 | const supported_comptime_for_kinds = ['methods', 'fields', 'values', 'variants', 'attributes', |
| 16 | 'params'] |
| 17 | const comptime_types = ['map', 'array', 'array_dynamic', 'array_fixed', 'int', 'float', 'struct', |
| 18 | 'interface', 'enum', 'sumtype', 'alias', 'function', 'option', 'shared', 'string', 'pointer', |
| 19 | 'voidptr'] |
| 20 | |
| 21 | fn find_veb_template_relative_to_vmod(compiled_vfile_path string, relative_path string) ?string { |
| 22 | mut mcache := vmod.get_cache() |
| 23 | vmod_file_location := mcache.get_by_file(compiled_vfile_path) |
| 24 | if vmod_file_location.vmod_file == '' { |
| 25 | return none |
| 26 | } |
| 27 | vmod_template_path := os.join_path(vmod_file_location.vmod_folder, relative_path) |
| 28 | if !os.exists(vmod_template_path) { |
| 29 | return none |
| 30 | } |
| 31 | return os.real_path(vmod_template_path) |
| 32 | } |
| 33 | |
| 34 | fn find_existing_veb_template_in_vmod(compiled_vfile_path string, relative_paths []string) ?string { |
| 35 | for relative_path in relative_paths { |
| 36 | if path := find_veb_template_relative_to_vmod(compiled_vfile_path, relative_path) { |
| 37 | return path |
| 38 | } |
| 39 | } |
| 40 | return none |
| 41 | } |
| 42 | |
| 43 | @[inline] |
| 44 | fn is_supported_comptime_for_kind(name string) bool { |
| 45 | return name in supported_comptime_for_kinds |
| 46 | } |
| 47 | |
| 48 | fn (mut p Parser) parse_comptime_type() ast.ComptimeType { |
| 49 | pos := p.tok.pos() |
| 50 | p.check(.dollar) |
| 51 | name := p.check_name() |
| 52 | if name !in comptime_types { |
| 53 | p.error('unsupported compile-time type `${name}`: only ${comptime_types} are supported') |
| 54 | } |
| 55 | mut kind := ast.ComptimeTypeKind.unknown |
| 56 | kind = match name { |
| 57 | 'map' { |
| 58 | .map |
| 59 | } |
| 60 | 'struct' { |
| 61 | .struct |
| 62 | } |
| 63 | 'interface' { |
| 64 | .iface |
| 65 | } |
| 66 | 'int' { |
| 67 | .int |
| 68 | } |
| 69 | 'float' { |
| 70 | .float |
| 71 | } |
| 72 | 'alias' { |
| 73 | .alias |
| 74 | } |
| 75 | 'function' { |
| 76 | .function |
| 77 | } |
| 78 | 'array' { |
| 79 | .array |
| 80 | } |
| 81 | 'array_fixed' { |
| 82 | .array_fixed |
| 83 | } |
| 84 | 'array_dynamic' { |
| 85 | .array_dynamic |
| 86 | } |
| 87 | 'enum' { |
| 88 | .enum |
| 89 | } |
| 90 | 'sumtype' { |
| 91 | .sum_type |
| 92 | } |
| 93 | 'option' { |
| 94 | .option |
| 95 | } |
| 96 | 'shared' { |
| 97 | .shared |
| 98 | } |
| 99 | 'string' { |
| 100 | .string |
| 101 | } |
| 102 | 'pointer' { |
| 103 | .pointer |
| 104 | } |
| 105 | 'voidptr' { |
| 106 | .voidptr |
| 107 | } |
| 108 | else { |
| 109 | .unknown |
| 110 | } |
| 111 | } |
| 112 | |
| 113 | return ast.ComptimeType{ |
| 114 | kind: kind |
| 115 | pos: pos |
| 116 | } |
| 117 | } |
| 118 | |
| 119 | // // #include, #flag, #v |
| 120 | fn (mut p Parser) hash() ast.HashStmt { |
| 121 | pos := p.tok.pos() |
| 122 | val := p.tok.lit |
| 123 | kind := val.all_before(' ') |
| 124 | attrs := p.attrs |
| 125 | p.next() |
| 126 | mut main_str := '' |
| 127 | mut msg := '' |
| 128 | mut ct_low_level_cond := '' |
| 129 | content := val.all_after('${kind} ').all_before('//') |
| 130 | if content.contains(' #') { |
| 131 | main_str = content.all_before(' #').trim_space() |
| 132 | msg = content.all_after(' #').trim_space() |
| 133 | } else { |
| 134 | main_str = content.trim_space() |
| 135 | msg = '' |
| 136 | } |
| 137 | |
| 138 | // Detect OS-specific conditions like `#include linux <pty.h>` or `#include darwin "util.h"`. |
| 139 | // The condition is the first word before the actual include path. |
| 140 | if kind in ['include', 'preinclude', 'postinclude', 'insert'] { |
| 141 | first_space := main_str.index_u8(` `) |
| 142 | if first_space > 0 { |
| 143 | maybe_cond := main_str[..first_space] |
| 144 | if maybe_cond in ast.valid_comptime_not_user_defined { |
| 145 | ct_low_level_cond = maybe_cond |
| 146 | main_str = main_str[first_space + 1..].trim_space() |
| 147 | } |
| 148 | } |
| 149 | } |
| 150 | |
| 151 | mut is_use_once := false |
| 152 | for fna in attrs { |
| 153 | match fna.name { |
| 154 | 'use_once' { is_use_once = true } |
| 155 | else {} |
| 156 | } |
| 157 | } |
| 158 | |
| 159 | return ast.HashStmt{ |
| 160 | mod: p.mod |
| 161 | source_file: p.file_path |
| 162 | val: val |
| 163 | kind: kind |
| 164 | main: main_str |
| 165 | msg: msg |
| 166 | pos: pos |
| 167 | attrs: attrs |
| 168 | is_use_once: is_use_once |
| 169 | ct_low_level_cond: ct_low_level_cond |
| 170 | } |
| 171 | } |
| 172 | |
| 173 | const error_msg = 'only `\$tmpl()`, `\$env()`, `\$embed_file()`, `\$pkgconfig()`, `\$veb.html()`, `\$compile_error()`, `\$compile_warn()`, `\$d()`, `\$res()`, `\$zero()` and `\$new()` comptime functions are supported right now' |
| 174 | |
| 175 | fn (p &Parser) resolve_tmpl_path_expr(expr ast.Expr) ?string { |
| 176 | return p.resolve_tmpl_path_expr_with_depth(expr, 0) |
| 177 | } |
| 178 | |
| 179 | fn (p &Parser) resolve_tmpl_path_expr_with_depth(expr ast.Expr, depth int) ?string { |
| 180 | if depth > 50 { |
| 181 | return none |
| 182 | } |
| 183 | match expr { |
| 184 | ast.AtExpr { |
| 185 | return expr.name |
| 186 | } |
| 187 | ast.CallExpr { |
| 188 | match expr.name { |
| 189 | 'os.join_path' { |
| 190 | if expr.args.len == 0 { |
| 191 | return '' |
| 192 | } |
| 193 | mut path := p.resolve_tmpl_path_expr_with_depth(expr.args[0].expr, depth + 1)? |
| 194 | for arg in expr.args[1..] { |
| 195 | path = os.join_path_single(path, p.resolve_tmpl_path_expr_with_depth(arg.expr, |
| 196 | |
| 197 | depth + 1)?) |
| 198 | } |
| 199 | return path |
| 200 | } |
| 201 | 'os.join_path_single' { |
| 202 | if expr.args.len != 2 { |
| 203 | return none |
| 204 | } |
| 205 | return os.join_path_single(p.resolve_tmpl_path_expr_with_depth(expr.args[0].expr, |
| 206 | |
| 207 | depth + 1)?, p.resolve_tmpl_path_expr_with_depth(expr.args[1].expr, depth + |
| 208 | 1)?) |
| 209 | } |
| 210 | else { |
| 211 | return none |
| 212 | } |
| 213 | } |
| 214 | } |
| 215 | ast.Ident { |
| 216 | if var := p.scope.find_var(expr.name) { |
| 217 | return p.resolve_tmpl_path_expr_with_depth(var.expr, depth + 1) |
| 218 | } |
| 219 | if expr.name == 'os.path_separator' { |
| 220 | return os.path_separator |
| 221 | } |
| 222 | if imported_name := p.imported_symbols[expr.name] { |
| 223 | if const_field := p.table.global_scope.find_const(imported_name) { |
| 224 | return p.resolve_tmpl_path_expr_with_depth(const_field.expr, depth + 1) |
| 225 | } |
| 226 | } |
| 227 | if const_field := p.table.global_scope.find_const(expr.name) { |
| 228 | return p.resolve_tmpl_path_expr_with_depth(const_field.expr, depth + 1) |
| 229 | } |
| 230 | if const_field := p.table.global_scope.find_const('${expr.mod}.${expr.name}') { |
| 231 | return p.resolve_tmpl_path_expr_with_depth(const_field.expr, depth + 1) |
| 232 | } |
| 233 | if const_field := p.table.global_scope.find_const('${p.mod}.${expr.name}') { |
| 234 | return p.resolve_tmpl_path_expr_with_depth(const_field.expr, depth + 1) |
| 235 | } |
| 236 | return none |
| 237 | } |
| 238 | ast.InfixExpr { |
| 239 | if expr.op != .plus { |
| 240 | return none |
| 241 | } |
| 242 | return p.resolve_tmpl_path_expr_with_depth(expr.left, depth + 1)? + |
| 243 | p.resolve_tmpl_path_expr_with_depth(expr.right, depth + 1)? |
| 244 | } |
| 245 | ast.ParExpr { |
| 246 | return p.resolve_tmpl_path_expr_with_depth(expr.expr, depth + 1) |
| 247 | } |
| 248 | ast.SelectorExpr { |
| 249 | module_name := p.resolve_tmpl_path_module_name(expr.expr) or { return none } |
| 250 | if module_name == 'os' && expr.field_name == 'path_separator' { |
| 251 | return os.path_separator |
| 252 | } |
| 253 | if const_field := p.table.global_scope.find_const('${module_name}.${expr.field_name}') { |
| 254 | return p.resolve_tmpl_path_expr_with_depth(const_field.expr, depth + 1) |
| 255 | } |
| 256 | return none |
| 257 | } |
| 258 | ast.StringLiteral { |
| 259 | return expr.val |
| 260 | } |
| 261 | else { |
| 262 | return none |
| 263 | } |
| 264 | } |
| 265 | } |
| 266 | |
| 267 | fn (p &Parser) resolve_tmpl_path_module_name(expr ast.Expr) ?string { |
| 268 | match expr { |
| 269 | ast.Ident { |
| 270 | if module_name := p.imports[expr.name] { |
| 271 | return module_name |
| 272 | } |
| 273 | return expr.name |
| 274 | } |
| 275 | ast.SelectorExpr { |
| 276 | return '${p.resolve_tmpl_path_module_name(expr.expr)?}.${expr.field_name}' |
| 277 | } |
| 278 | else { |
| 279 | return none |
| 280 | } |
| 281 | } |
| 282 | } |
| 283 | |
| 284 | fn (mut p Parser) resolve_tmpl_pseudo_variables(path string, pos token.Pos) ?string { |
| 285 | mut resolved := path |
| 286 | source_path := os.real_path(p.scanner.file_path) |
| 287 | if resolved.contains('@VEXEROOT') { |
| 288 | resolved = resolved.replace('@VEXEROOT', p.pref.vroot) |
| 289 | } |
| 290 | if resolved.contains('@VMODROOT') { |
| 291 | resolved = util.resolve_vmodroot(resolved, source_path) or { |
| 292 | p.error_with_pos(err.msg(), pos) |
| 293 | return none |
| 294 | } |
| 295 | } |
| 296 | if resolved.contains('@DIR') { |
| 297 | resolved = resolved.replace('@DIR', os.dir(source_path)) |
| 298 | } |
| 299 | return resolved |
| 300 | } |
| 301 | |
| 302 | fn (p &Parser) is_comptime_type_selector_at(offset int) bool { |
| 303 | if p.peek_token(offset).kind == .key_typeof { |
| 304 | return true |
| 305 | } |
| 306 | if p.peek_token(offset).kind != .name { |
| 307 | return false |
| 308 | } |
| 309 | mut n := offset + 1 |
| 310 | for p.peek_token(n).kind == .dot && p.peek_token(n + 1).kind == .name { |
| 311 | if is_array_init_type_expr_field(p.peek_token(n + 1).lit) { |
| 312 | return true |
| 313 | } |
| 314 | n += 2 |
| 315 | } |
| 316 | return false |
| 317 | } |
| 318 | |
| 319 | fn (p &Parser) is_comptime_type_expr_arg() bool { |
| 320 | if p.tok.kind in [.key_typeof, .dollar] { |
| 321 | return true |
| 322 | } |
| 323 | if p.tok.kind == .lsbr && p.peek_tok.kind == .rsbr { |
| 324 | return p.is_comptime_type_selector_at(2) |
| 325 | } |
| 326 | return p.is_comptime_type_selector_at(0) |
| 327 | } |
| 328 | |
| 329 | fn (mut p Parser) comptime_call_type_arg() ast.Expr { |
| 330 | arg_pos := p.tok.pos() |
| 331 | if p.is_comptime_type_expr_arg() { |
| 332 | old_inside_array_init_type_expr := p.inside_array_init_type_expr |
| 333 | p.inside_array_init_type_expr = true |
| 334 | expr := p.expr(0) |
| 335 | p.inside_array_init_type_expr = old_inside_array_init_type_expr |
| 336 | return expr |
| 337 | } |
| 338 | typ := p.parse_type() |
| 339 | return ast.TypeNode{ |
| 340 | typ: typ |
| 341 | pos: arg_pos.extend(p.prev_tok.pos()) |
| 342 | } |
| 343 | } |
| 344 | |
| 345 | fn (mut p Parser) comptime_call() ast.ComptimeCall { |
| 346 | err_node := ast.ComptimeCall{ |
| 347 | scope: unsafe { nil } |
| 348 | kind: .unknown |
| 349 | } |
| 350 | start_pos := p.tok.pos() |
| 351 | p.check(.dollar) |
| 352 | mut is_veb := false |
| 353 | if p.peek_tok.kind == .dot { |
| 354 | name := p.check_name() |
| 355 | if name != 'veb' { |
| 356 | p.error(error_msg) |
| 357 | return err_node |
| 358 | } |
| 359 | import_mods := p.ast_imports.map(it.mod) |
| 360 | if 'veb' !in import_mods { |
| 361 | p.error_with_pos('`\$veb` cannot be used without importing veb', |
| 362 | start_pos.extend(p.prev_tok.pos())) |
| 363 | return err_node |
| 364 | } |
| 365 | p.register_used_import('veb') |
| 366 | is_veb = true |
| 367 | p.check(.dot) |
| 368 | } |
| 369 | method_name := p.check_name() |
| 370 | if method_name !in supported_comptime_calls { |
| 371 | p.error(error_msg) |
| 372 | return err_node |
| 373 | } |
| 374 | is_embed_file := method_name == 'embed_file' |
| 375 | is_html := method_name == 'html' |
| 376 | p.check(.lpar) |
| 377 | arg_pos := p.tok.pos() |
| 378 | if method_name in ['env', 'pkgconfig'] { |
| 379 | s := p.tok.lit |
| 380 | p.check(.string) |
| 381 | p.check(.rpar) |
| 382 | is_env := method_name == 'env' |
| 383 | return ast.ComptimeCall{ |
| 384 | scope: unsafe { nil } |
| 385 | method_name: method_name |
| 386 | kind: if is_env { .env } else { .pkgconfig } |
| 387 | args_var: s |
| 388 | env_pos: start_pos |
| 389 | pos: start_pos.extend(p.prev_tok.pos()) |
| 390 | } |
| 391 | } else if method_name in ['compile_error', 'compile_warn'] { |
| 392 | mut s := '' |
| 393 | mut args := []ast.CallArg{} |
| 394 | if p.tok.kind == .string && p.peek_tok.kind == .rpar { |
| 395 | s = p.tok.lit |
| 396 | p.check(.string) |
| 397 | } else { |
| 398 | args << ast.CallArg{ |
| 399 | expr: p.string_expr() |
| 400 | typ: ast.string_type |
| 401 | ct_expr: true |
| 402 | } |
| 403 | } |
| 404 | p.check(.rpar) |
| 405 | return ast.ComptimeCall{ |
| 406 | scope: unsafe { nil } |
| 407 | method_name: method_name |
| 408 | kind: if method_name == 'compile_error' { .compile_error } else { .compile_warn } |
| 409 | args_var: s |
| 410 | env_pos: start_pos |
| 411 | pos: start_pos.extend(p.prev_tok.pos()) |
| 412 | args: args |
| 413 | } |
| 414 | } else if method_name == 'res' { |
| 415 | mut has_args := false |
| 416 | mut type_index := '' |
| 417 | if p.tok.kind == .number { |
| 418 | has_args = true |
| 419 | type_index = p.tok.lit |
| 420 | p.check(.number) |
| 421 | } |
| 422 | p.check(.rpar) |
| 423 | if has_args { |
| 424 | return ast.ComptimeCall{ |
| 425 | scope: unsafe { nil } |
| 426 | method_name: method_name |
| 427 | kind: .res |
| 428 | args_var: type_index |
| 429 | pos: start_pos.extend(p.prev_tok.pos()) |
| 430 | } |
| 431 | } |
| 432 | return ast.ComptimeCall{ |
| 433 | scope: unsafe { nil } |
| 434 | method_name: method_name |
| 435 | kind: .res |
| 436 | pos: start_pos.extend(p.prev_tok.pos()) |
| 437 | } |
| 438 | } else if method_name == 'd' { |
| 439 | const_string := p.tok.lit |
| 440 | // const_name_pos := p.tok.pos() |
| 441 | p.check(.string) |
| 442 | p.check(.comma) |
| 443 | arg_expr := p.expr(0) |
| 444 | args := [ |
| 445 | ast.CallArg{ |
| 446 | expr: arg_expr |
| 447 | pos: p.tok.pos() |
| 448 | }, |
| 449 | ] |
| 450 | p.check(.rpar) |
| 451 | return ast.ComptimeCall{ |
| 452 | scope: unsafe { nil } |
| 453 | method_name: method_name |
| 454 | kind: .d |
| 455 | args_var: const_string |
| 456 | args: args |
| 457 | pos: start_pos.extend(p.prev_tok.pos()) |
| 458 | } |
| 459 | } else if method_name in ['zero', 'new'] { |
| 460 | arg_expr := p.comptime_call_type_arg() |
| 461 | p.check(.rpar) |
| 462 | return ast.ComptimeCall{ |
| 463 | scope: unsafe { nil } |
| 464 | method_name: method_name |
| 465 | kind: if method_name == 'zero' { .zero } else { .new } |
| 466 | args: [ |
| 467 | ast.CallArg{ |
| 468 | expr: arg_expr |
| 469 | pos: arg_pos |
| 470 | }, |
| 471 | ] |
| 472 | pos: start_pos.extend(p.prev_tok.pos()) |
| 473 | } |
| 474 | } |
| 475 | has_string_arg := p.tok.kind == .string |
| 476 | mut literal_string_param := if is_html && !has_string_arg { '' } else { p.tok.lit } |
| 477 | mut arg := ast.CallArg{} |
| 478 | if is_html && !(has_string_arg || p.tok.kind == .rpar) { |
| 479 | p.error('expecting `\$veb.html()` for a default template path or `\$veb.html("/path/to/template.html")`') |
| 480 | } |
| 481 | if is_html && p.tok.kind != .string { |
| 482 | // $veb.html() can have no arguments |
| 483 | } else { |
| 484 | arg_expr := p.expr(0) |
| 485 | if resolved_path := p.resolve_tmpl_path_expr(arg_expr) { |
| 486 | literal_string_param = resolved_path |
| 487 | } |
| 488 | arg = ast.CallArg{ |
| 489 | expr: arg_expr |
| 490 | } |
| 491 | } |
| 492 | mut embed_compression_type := 'none' |
| 493 | if is_embed_file { |
| 494 | if p.tok.kind == .comma { |
| 495 | p.next() |
| 496 | p.check(.dot) |
| 497 | embed_compression_type = p.check_name() |
| 498 | } |
| 499 | } |
| 500 | p.check(.rpar) |
| 501 | // $embed_file('/path/to/file') |
| 502 | if is_embed_file { |
| 503 | p.register_auto_import('v.preludes.embed_file') |
| 504 | if embed_compression_type == 'zlib' { |
| 505 | p.register_auto_import('v.preludes.embed_file.zlib') |
| 506 | } |
| 507 | return ast.ComptimeCall{ |
| 508 | scope: unsafe { nil } |
| 509 | method_name: method_name |
| 510 | kind: .embed_file |
| 511 | embed_file: ast.EmbeddedFile{ |
| 512 | compression_type: embed_compression_type |
| 513 | } |
| 514 | args: [arg] |
| 515 | pos: start_pos.extend(p.prev_tok.pos()) |
| 516 | } |
| 517 | } |
| 518 | // Compile veb html template to V code, parse that V code and embed the resulting V function |
| 519 | // that returns an html string. |
| 520 | fn_path := p.cur_fn_name.split('_') |
| 521 | fn_path_joined := fn_path.join(os.path_separator) |
| 522 | fn_name_html := '${p.cur_fn_name}.html' |
| 523 | compiled_vfile_path := os.real_path(p.scanner.file_path.replace('/', os.path_separator)) |
| 524 | tmpl_path := if is_html && !has_string_arg { |
| 525 | '${fn_path.last()}.html' |
| 526 | } else { |
| 527 | mut resolved_path := literal_string_param.replace('/', os.path_separator) |
| 528 | resolved_path = p.resolve_tmpl_pseudo_variables(resolved_path, arg_pos) or { |
| 529 | return err_node |
| 530 | } |
| 531 | resolved_path |
| 532 | } |
| 533 | // Looking next to the veb program |
| 534 | dir := os.dir(compiled_vfile_path) |
| 535 | mut path := os.join_path_single(dir, fn_path_joined) |
| 536 | path += '.html' |
| 537 | if !is_html || has_string_arg { |
| 538 | if os.is_abs_path(tmpl_path) { |
| 539 | path = tmpl_path |
| 540 | } else { |
| 541 | path = os.join_path_single(dir, tmpl_path) |
| 542 | } |
| 543 | } |
| 544 | if !os.exists(path) { |
| 545 | if is_html { |
| 546 | if has_string_arg && !os.is_abs_path(tmpl_path) { |
| 547 | path = find_veb_template_relative_to_vmod(compiled_vfile_path, tmpl_path) or { |
| 548 | path |
| 549 | } |
| 550 | } else { |
| 551 | flat_path := os.join_path_single(dir, fn_name_html) |
| 552 | if os.exists(flat_path) { |
| 553 | path = flat_path |
| 554 | } |
| 555 | if !os.exists(path) { |
| 556 | // can be in `templates/` |
| 557 | path = os.join_path(dir, 'templates', fn_path_joined) |
| 558 | path += '.html' |
| 559 | } |
| 560 | if !os.exists(path) { |
| 561 | flat_template_path := os.join_path(dir, 'templates', fn_name_html) |
| 562 | if os.exists(flat_template_path) { |
| 563 | path = flat_template_path |
| 564 | } |
| 565 | } |
| 566 | if !os.exists(path) { |
| 567 | // Same-module subdirs and `base_url` can place route handlers below the module |
| 568 | // root while keeping `templates/` next to `v.mod`. |
| 569 | vmod_relative_paths := [ |
| 570 | os.join_path('templates', fn_path_joined) + '.html', |
| 571 | os.join_path('templates', fn_name_html), |
| 572 | ] |
| 573 | path = find_existing_veb_template_in_vmod(compiled_vfile_path, |
| 574 | vmod_relative_paths) or { path } |
| 575 | } |
| 576 | } |
| 577 | } |
| 578 | if !os.exists(path) { |
| 579 | if p.pref.is_fmt { |
| 580 | return ast.ComptimeCall{ |
| 581 | scope: unsafe { nil } |
| 582 | is_template: true |
| 583 | is_veb: is_veb |
| 584 | method_name: method_name |
| 585 | kind: if is_html { .html } else { .tmpl } |
| 586 | args_var: literal_string_param |
| 587 | args: [arg] |
| 588 | pos: start_pos.extend(p.prev_tok.pos()) |
| 589 | } |
| 590 | } |
| 591 | if is_html { |
| 592 | p.error_with_pos('veb HTML template "${tmpl_path}" not found', arg_pos) |
| 593 | } else { |
| 594 | p.error_with_pos('template file "${tmpl_path}" not found', arg_pos) |
| 595 | } |
| 596 | return err_node |
| 597 | } |
| 598 | // println('path is now "$path"') |
| 599 | } |
| 600 | tmp_fn_name := p.cur_fn_name.replace('.', '__').to_lower() + start_pos.pos.str() |
| 601 | $if trace_comptime ? { |
| 602 | println('>>> compiling comptime template file "${path}" for ${tmp_fn_name}') |
| 603 | } |
| 604 | v_code := p.compile_template_file(path, tmp_fn_name) |
| 605 | $if print_veb_template_expansions ? { |
| 606 | lines := v_code.split('\n') |
| 607 | for i, line in lines { |
| 608 | println('${path}:${i + 1}: ${line}') |
| 609 | } |
| 610 | } |
| 611 | $if trace_comptime ? { |
| 612 | println('') |
| 613 | println('>>> template for ${path}:') |
| 614 | println(v_code) |
| 615 | println('>>> end of template END') |
| 616 | println('') |
| 617 | } |
| 618 | // the tmpl inherits all parent scopes. previous functionality was just to |
| 619 | // inherit the scope from which the comptime call was made and no parents. |
| 620 | // this is much simpler and allows access to globals. can be changed if needed. |
| 621 | p.open_scope() |
| 622 | defer { |
| 623 | p.close_scope() |
| 624 | } |
| 625 | mut file := parse_comptime(tmpl_path, v_code, mut p.table, p.pref, mut p.scope) |
| 626 | file.path = tmpl_path |
| 627 | template_call_stack := [ |
| 628 | errors.CallStackItem{ |
| 629 | file_path: p.file_path |
| 630 | pos: start_pos |
| 631 | }, |
| 632 | ] |
| 633 | // Store call stack info for template errors |
| 634 | file.call_stack = template_call_stack |
| 635 | // Transfer template paths and line mapping from parser to file for error reporting |
| 636 | file.template_paths = p.template_paths |
| 637 | file.template_line_map = p.template_line_map |
| 638 | for i, err in file.errors { |
| 639 | mut file_path := err.file_path |
| 640 | mut line_nr := err.pos.line_nr |
| 641 | if err.pos.line_nr >= 0 && err.pos.line_nr < file.template_line_map.len { |
| 642 | line_info := file.template_line_map[err.pos.line_nr] |
| 643 | file_path = line_info.tmpl_path |
| 644 | line_nr = line_info.tmpl_line |
| 645 | } |
| 646 | file.errors[i] = errors.Error{ |
| 647 | message: err.message |
| 648 | details: err.details |
| 649 | file_path: file_path |
| 650 | pos: token.Pos{ |
| 651 | ...err.pos |
| 652 | line_nr: line_nr |
| 653 | } |
| 654 | reporter: err.reporter |
| 655 | call_stack: if err.call_stack.len > 0 { err.call_stack } else { template_call_stack } |
| 656 | } |
| 657 | } |
| 658 | for i, warn in file.warnings { |
| 659 | mut file_path := warn.file_path |
| 660 | mut line_nr := warn.pos.line_nr |
| 661 | if warn.pos.line_nr >= 0 && warn.pos.line_nr < file.template_line_map.len { |
| 662 | line_info := file.template_line_map[warn.pos.line_nr] |
| 663 | file_path = line_info.tmpl_path |
| 664 | line_nr = line_info.tmpl_line |
| 665 | } |
| 666 | file.warnings[i] = errors.Warning{ |
| 667 | message: warn.message |
| 668 | details: warn.details |
| 669 | file_path: file_path |
| 670 | pos: token.Pos{ |
| 671 | ...warn.pos |
| 672 | line_nr: line_nr |
| 673 | } |
| 674 | reporter: warn.reporter |
| 675 | call_stack: if warn.call_stack.len > 0 { warn.call_stack } else { template_call_stack } |
| 676 | } |
| 677 | } |
| 678 | for i, notice in file.notices { |
| 679 | mut file_path := notice.file_path |
| 680 | mut line_nr := notice.pos.line_nr |
| 681 | if notice.pos.line_nr >= 0 && notice.pos.line_nr < file.template_line_map.len { |
| 682 | line_info := file.template_line_map[notice.pos.line_nr] |
| 683 | file_path = line_info.tmpl_path |
| 684 | line_nr = line_info.tmpl_line |
| 685 | } |
| 686 | file.notices[i] = errors.Notice{ |
| 687 | message: notice.message |
| 688 | details: notice.details |
| 689 | file_path: file_path |
| 690 | pos: token.Pos{ |
| 691 | ...notice.pos |
| 692 | line_nr: line_nr |
| 693 | } |
| 694 | reporter: notice.reporter |
| 695 | call_stack: if notice.call_stack.len > 0 { |
| 696 | notice.call_stack |
| 697 | } else { |
| 698 | template_call_stack |
| 699 | } |
| 700 | } |
| 701 | } |
| 702 | return ast.ComptimeCall{ |
| 703 | scope: unsafe { nil } |
| 704 | is_template: true |
| 705 | is_veb: is_veb |
| 706 | veb_tmpl: file |
| 707 | method_name: method_name |
| 708 | kind: if is_html { .html } else { .tmpl } |
| 709 | args_var: literal_string_param |
| 710 | args: [arg] |
| 711 | pos: start_pos.extend(p.prev_tok.pos()) |
| 712 | } |
| 713 | } |
| 714 | |
| 715 | fn (mut p Parser) comptime_for() ast.ComptimeFor { |
| 716 | // p.comptime_for() handles these special forms: |
| 717 | // `$for method in App.methods {` |
| 718 | // `$for val in App.values {` |
| 719 | // `$for field in App.fields {` |
| 720 | // `$for attr in App.attributes {` |
| 721 | // `$for variant in App.variants {` |
| 722 | // `$for variant in field.typ.variants {` |
| 723 | p.next() |
| 724 | p.check(.key_for) |
| 725 | var_pos := p.tok.pos() |
| 726 | val_var := p.check_name() |
| 727 | p.check(.key_in) |
| 728 | mut expr := ast.empty_expr |
| 729 | mut typ_pos := p.tok.pos() |
| 730 | lang := p.parse_language() |
| 731 | mut typ := ast.void_type |
| 732 | |
| 733 | if p.tok.lit.len == 0 { |
| 734 | p.error('invalid expr, use `${p.peek_tok.lit}` instead') |
| 735 | return ast.ComptimeFor{} |
| 736 | } |
| 737 | if p.tok.lit[0].is_capital() || p.tok.lit in p.imports { |
| 738 | typ = p.parse_any_type(lang, false, true, false) |
| 739 | } else { |
| 740 | mut selector_expr := ast.Expr(p.ident(lang)) |
| 741 | p.scope.mark_var_as_used((selector_expr as ast.Ident).name) |
| 742 | for p.tok.kind == .dot && p.peek_tok.kind == .name |
| 743 | && !is_supported_comptime_for_kind(p.peek_tok.lit) { |
| 744 | selector_expr = p.dot_expr(selector_expr) |
| 745 | if p.name_error { |
| 746 | return ast.ComptimeFor{} |
| 747 | } |
| 748 | if selector_expr !is ast.SelectorExpr { |
| 749 | p.error_with_pos('invalid expr, use a selector like `field.typ`', |
| 750 | selector_expr.pos()) |
| 751 | return ast.ComptimeFor{} |
| 752 | } |
| 753 | } |
| 754 | expr = selector_expr |
| 755 | } |
| 756 | typ_pos = typ_pos.extend(p.prev_tok.pos()) |
| 757 | p.check(.dot) |
| 758 | for_val := p.check_name() |
| 759 | mut kind := ast.ComptimeForKind.methods |
| 760 | p.open_scope() |
| 761 | defer { |
| 762 | p.close_scope() |
| 763 | } |
| 764 | match for_val { |
| 765 | 'params' { |
| 766 | p.scope.register(ast.Var{ |
| 767 | name: val_var |
| 768 | typ: p.table.find_type('FunctionParam') |
| 769 | pos: var_pos |
| 770 | }) |
| 771 | kind = .params |
| 772 | } |
| 773 | 'methods' { |
| 774 | p.scope.register(ast.Var{ |
| 775 | name: val_var |
| 776 | typ: p.table.find_type('FunctionData') |
| 777 | pos: var_pos |
| 778 | }) |
| 779 | } |
| 780 | 'values' { |
| 781 | p.scope.register(ast.Var{ |
| 782 | name: val_var |
| 783 | typ: p.table.find_type('EnumData') |
| 784 | pos: var_pos |
| 785 | }) |
| 786 | kind = .values |
| 787 | } |
| 788 | 'fields' { |
| 789 | p.scope.register(ast.Var{ |
| 790 | name: val_var |
| 791 | typ: p.table.find_type('FieldData') |
| 792 | pos: var_pos |
| 793 | }) |
| 794 | kind = .fields |
| 795 | } |
| 796 | 'variants' { |
| 797 | p.scope.register(ast.Var{ |
| 798 | name: val_var |
| 799 | typ: p.table.find_type('VariantData') |
| 800 | pos: var_pos |
| 801 | }) |
| 802 | kind = .variants |
| 803 | } |
| 804 | 'attributes' { |
| 805 | p.scope.register(ast.Var{ |
| 806 | name: val_var |
| 807 | typ: p.table.find_type('VAttribute') |
| 808 | pos: var_pos |
| 809 | }) |
| 810 | kind = .attributes |
| 811 | } |
| 812 | else { |
| 813 | p.error_with_pos('unknown kind `${for_val}`, available are: `methods`, `fields`, `values`, `variants`, `attributes` or `params`', |
| 814 | p.prev_tok.pos()) |
| 815 | return ast.ComptimeFor{} |
| 816 | } |
| 817 | } |
| 818 | |
| 819 | spos := p.tok.pos() |
| 820 | stmts := p.parse_block() |
| 821 | return ast.ComptimeFor{ |
| 822 | val_var: val_var |
| 823 | stmts: stmts |
| 824 | kind: kind |
| 825 | typ: typ |
| 826 | expr: expr |
| 827 | typ_pos: typ_pos |
| 828 | scope: p.scope |
| 829 | pos: spos.extend(p.tok.pos()) |
| 830 | } |
| 831 | } |
| 832 | |
| 833 | // @FN, @STRUCT, @MOD etc. See full list in token.valid_at_tokens |
| 834 | fn (mut p Parser) at() ast.AtExpr { |
| 835 | name := p.tok.lit |
| 836 | kind := match name { |
| 837 | '@FN' { token.AtKind.fn_name } |
| 838 | '@METHOD' { token.AtKind.method_name } |
| 839 | '@MOD' { token.AtKind.mod_name } |
| 840 | '@STRUCT' { token.AtKind.struct_name } |
| 841 | '@FILE' { token.AtKind.file_path } |
| 842 | '@DIR' { token.AtKind.file_dir } |
| 843 | '@LINE' { token.AtKind.line_nr } |
| 844 | '@FILE_LINE' { token.AtKind.file_path_line_nr } |
| 845 | '@LOCATION' { token.AtKind.location } |
| 846 | '@COLUMN' { token.AtKind.column_nr } |
| 847 | '@VCURRENTHASH' { token.AtKind.v_current_hash } |
| 848 | '@VHASH' { token.AtKind.vhash } |
| 849 | '@VMOD_FILE' { token.AtKind.vmod_file } |
| 850 | '@VEXE' { token.AtKind.vexe_path } |
| 851 | '@VEXEROOT' { token.AtKind.vexeroot_path } |
| 852 | '@VMODROOT' { token.AtKind.vmodroot_path } |
| 853 | '@VMODHASH' { token.AtKind.vmod_hash } |
| 854 | '@VROOT' { token.AtKind.vroot_path } // deprecated, use @VEXEROOT or @VMODROOT |
| 855 | '@BUILD_DATE' { token.AtKind.build_date } |
| 856 | '@BUILD_TIME' { token.AtKind.build_time } |
| 857 | '@BUILD_TIMESTAMP' { token.AtKind.build_timestamp } |
| 858 | '@OS' { token.AtKind.os } |
| 859 | '@CCOMPILER' { token.AtKind.ccompiler } |
| 860 | '@BACKEND' { token.AtKind.backend } |
| 861 | '@PLATFORM' { token.AtKind.platform } |
| 862 | else { token.AtKind.unknown } |
| 863 | } |
| 864 | |
| 865 | expr := ast.AtExpr{ |
| 866 | name: name |
| 867 | pos: p.tok.pos() |
| 868 | kind: kind |
| 869 | } |
| 870 | p.next() |
| 871 | return expr |
| 872 | } |
| 873 | |
| 874 | fn (mut p Parser) comptime_selector(left ast.Expr) ast.Expr { |
| 875 | p.check(.dollar) |
| 876 | start_pos := p.prev_tok.pos() |
| 877 | if p.peek_tok.kind == .lpar { |
| 878 | method_pos := p.tok.pos() |
| 879 | method_name := p.check_name() |
| 880 | p.scope.mark_var_as_used(method_name) |
| 881 | // `app.$action()` (`action` is a string) |
| 882 | p.check(.lpar) |
| 883 | args := p.call_args() |
| 884 | p.check(.rpar) |
| 885 | mut or_kind := ast.OrKind.absent |
| 886 | mut or_pos := p.tok.pos() |
| 887 | mut or_stmts := []ast.Stmt{} |
| 888 | mut or_scope := ast.empty_scope |
| 889 | if p.tok.kind == .key_orelse { |
| 890 | // `$method() or {}`` |
| 891 | or_kind = .block |
| 892 | or_stmts, or_pos, or_scope = p.or_block(.with_err_var) |
| 893 | } |
| 894 | return ast.ComptimeCall{ |
| 895 | left: left |
| 896 | method_name: method_name |
| 897 | kind: .method |
| 898 | method_pos: method_pos |
| 899 | scope: p.scope |
| 900 | args_var: '' |
| 901 | args: args |
| 902 | pos: start_pos.extend(p.prev_tok.pos()) |
| 903 | or_block: ast.OrExpr{ |
| 904 | stmts: or_stmts |
| 905 | kind: or_kind |
| 906 | pos: or_pos |
| 907 | scope: or_scope |
| 908 | } |
| 909 | } |
| 910 | } |
| 911 | mut has_parens := false |
| 912 | if p.tok.kind == .lpar { |
| 913 | p.next() |
| 914 | has_parens = true |
| 915 | } else { |
| 916 | p.warn_with_pos('use brackets instead e.g. `s.$(field.name)` - run vfmt', p.tok.pos()) |
| 917 | } |
| 918 | expr := p.expr(0) |
| 919 | if has_parens { |
| 920 | p.check(.rpar) |
| 921 | } |
| 922 | return ast.ComptimeSelector{ |
| 923 | has_parens: has_parens |
| 924 | left: left |
| 925 | field_expr: expr |
| 926 | pos: start_pos.extend(p.prev_tok.pos()) |
| 927 | or_block: ast.OrExpr{ |
| 928 | stmts: []ast.Stmt{} |
| 929 | kind: if p.tok.kind == .question { .propagate_option } else { .absent } |
| 930 | scope: p.scope |
| 931 | pos: p.tok.pos() |
| 932 | } |
| 933 | } |
| 934 | } |
| 935 | |