v: add support for `v crun examples/hello_world.v`, use crun mode for .vsh files by default. (#14554)

master
Delyan Angelov 2022-06-01 14:47:52 +03:00 committed by GitHub
parent c91b646372
commit bf70f0b436
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 300 additions and 137 deletions

View File

@ -7,6 +7,7 @@ Examples:
v hello.v Compile the file `hello.v` and output it as `hello` or `hello.exe`. v hello.v Compile the file `hello.v` and output it as `hello` or `hello.exe`.
v run hello.v Same as above but also run the produced executable immediately after compilation. v run hello.v Same as above but also run the produced executable immediately after compilation.
v -cg run hello.v Same as above, but make debugging easier (in case your program crashes). v -cg run hello.v Same as above, but make debugging easier (in case your program crashes).
v crun hello.v Same as above, but do not recompile, if the executable already exists, and is newer than the sources.
v -o h.c hello.v Translate `hello.v` to `h.c`. Do not compile further. v -o h.c hello.v Translate `hello.v` to `h.c`. Do not compile further.
v -o - hello.v Translate `hello.v` and output the C source code to stdout. Do not compile further. v -o - hello.v Translate `hello.v` and output the C source code to stdout. Do not compile further.
@ -20,7 +21,10 @@ V supports the following commands:
init Setup the file structure for an already existing V project. init Setup the file structure for an already existing V project.
* Ordinary development: * Ordinary development:
run Compile and run a V program. run Compile and run a V program. Delete the executable after the run.
crun Compile and run a V program without deleting the executable.
If you run the same program a second time, without changing the source files,
V will just run the executable, without recompilation. Suitable for scripting.
test Run all test files in the provided directory. test Run all test files in the provided directory.
fmt Format the V code provided. fmt Format the V code provided.
vet Report suspicious code constructs. vet Report suspicious code constructs.

View File

@ -93,17 +93,21 @@ fn main() {
return return
} }
match command { match command {
'run', 'crun', 'build', 'build-module' {
rebuild(prefs)
return
}
'help' { 'help' {
invoke_help_and_exit(args) invoke_help_and_exit(args)
} }
'version' {
println(version.full_v_version(prefs.is_verbose))
return
}
'new', 'init' { 'new', 'init' {
util.launch_tool(prefs.is_verbose, 'vcreate', os.args[1..]) util.launch_tool(prefs.is_verbose, 'vcreate', os.args[1..])
return return
} }
'translate' {
eprintln('Translating C to V will be available in V 0.3')
exit(1)
}
'install', 'list', 'outdated', 'remove', 'search', 'show', 'update', 'upgrade' { 'install', 'list', 'outdated', 'remove', 'search', 'show', 'update', 'upgrade' {
util.launch_tool(prefs.is_verbose, 'vpm', os.args[1..]) util.launch_tool(prefs.is_verbose, 'vpm', os.args[1..])
return return
@ -118,15 +122,39 @@ fn main() {
eprintln('V Error: Use `v install` to install modules from vpm.vlang.io') eprintln('V Error: Use `v install` to install modules from vpm.vlang.io')
exit(1) exit(1)
} }
'version' { 'translate' {
println(version.full_v_version(prefs.is_verbose)) eprintln('Translating C to V will be available in V 0.3')
return exit(1)
} }
else {} else {
} if command.ends_with('.v') || os.exists(command) {
if command in ['run', 'build', 'build-module'] || command.ends_with('.v') || os.exists(command) {
// println('command') // println('command')
// println(prefs.path) // println(prefs.path)
rebuild(prefs)
return
}
}
}
if prefs.is_help {
invoke_help_and_exit(args)
}
eprintln('v $command: unknown command')
eprintln('Run ${term.highlight_command('v help')} for usage.')
exit(1)
}
fn invoke_help_and_exit(remaining []string) {
match remaining.len {
0, 1 { help.print_and_exit('default') }
2 { help.print_and_exit(remaining[1]) }
else {}
}
eprintln('${term.highlight_command('v help')}: provide only one help topic.')
eprintln('For usage information, use ${term.highlight_command('v help')}.')
exit(1)
}
fn rebuild(prefs &pref.Preferences) {
match prefs.backend { match prefs.backend {
.c { .c {
$if no_bootstrapv ? { $if no_bootstrapv ? {
@ -148,22 +176,4 @@ fn main() {
util.launch_tool(prefs.is_verbose, 'builders/interpret_builder', os.args[1..]) util.launch_tool(prefs.is_verbose, 'builders/interpret_builder', os.args[1..])
} }
} }
return
}
if prefs.is_help {
invoke_help_and_exit(args)
}
eprintln('v $command: unknown command\nRun ${term.highlight_command('v help')} for usage.')
exit(1)
}
fn invoke_help_and_exit(remaining []string) {
match remaining.len {
0, 1 { help.print_and_exit('default') }
2 { help.print_and_exit(remaining[1]) }
else {}
}
println('${term.highlight_command('v help')}: provide only one help topic.')
println('For usage information, use ${term.highlight_command('v help')}.')
exit(1)
} }

View File

@ -17,7 +17,7 @@ import v.dotgraph
pub struct Builder { pub struct Builder {
pub: pub:
compiled_dir string // contains os.real_path() of the dir of the final file beeing compiled, or the dir itself when doing `v .` compiled_dir string // contains os.real_path() of the dir of the final file being compiled, or the dir itself when doing `v .`
module_path string module_path string
pub mut: pub mut:
checker &checker.Checker checker &checker.Checker
@ -40,6 +40,7 @@ pub mut:
mod_invalidates_paths map[string][]string // changes in mod `os`, invalidate only .v files, that do `import os` mod_invalidates_paths map[string][]string // changes in mod `os`, invalidate only .v files, that do `import os`
mod_invalidates_mods map[string][]string // changes in mod `os`, force invalidation of mods, that do `import os` mod_invalidates_mods map[string][]string // changes in mod `os`, force invalidation of mods, that do `import os`
path_invalidates_mods map[string][]string // changes in a .v file from `os`, invalidates `os` path_invalidates_mods map[string][]string // changes in a .v file from `os`, invalidates `os`
crun_cache_keys []string // target executable + top level source files; filled in by Builder.should_rebuild
} }
pub fn new_builder(pref &pref.Preferences) Builder { pub fn new_builder(pref &pref.Preferences) Builder {

View File

@ -3,9 +3,7 @@
// that can be found in the LICENSE file. // that can be found in the LICENSE file.
module builder module builder
import time
import os import os
import rand
import v.pref import v.pref
import v.util import v.util
import v.checker import v.checker
@ -13,6 +11,19 @@ import v.checker
pub type FnBackend = fn (mut b Builder) pub type FnBackend = fn (mut b Builder)
pub fn compile(command string, pref &pref.Preferences, backend_cb FnBackend) { pub fn compile(command string, pref &pref.Preferences, backend_cb FnBackend) {
check_if_output_folder_is_writable(pref)
// Construct the V object from command line arguments
mut b := new_builder(pref)
if b.should_rebuild() {
b.rebuild(backend_cb)
}
b.exit_on_invalid_syntax()
// running does not require the parsers anymore
unsafe { b.myfree() }
b.run_compiled_executable_and_exit()
}
fn check_if_output_folder_is_writable(pref &pref.Preferences) {
odir := os.dir(pref.out_name) odir := os.dir(pref.out_name)
// When pref.out_name is just the name of an executable, i.e. `./v -o executable main.v` // When pref.out_name is just the name of an executable, i.e. `./v -o executable main.v`
// without a folder component, just use the current folder instead: // without a folder component, just use the current folder instead:
@ -24,56 +35,6 @@ pub fn compile(command string, pref &pref.Preferences, backend_cb FnBackend) {
// An early error here, is better than an unclear C error later: // An early error here, is better than an unclear C error later:
verror(err.msg()) verror(err.msg())
} }
// Construct the V object from command line arguments
mut b := new_builder(pref)
if pref.is_verbose {
println('builder.compile() pref:')
// println(pref)
}
mut sw := time.new_stopwatch()
backend_cb(mut b)
mut timers := util.get_timers()
timers.show_remaining()
if pref.is_stats {
compilation_time_micros := 1 + sw.elapsed().microseconds()
scompilation_time_ms := util.bold('${f64(compilation_time_micros) / 1000.0:6.3f}')
mut all_v_source_lines, mut all_v_source_bytes := 0, 0
for pf in b.parsed_files {
all_v_source_lines += pf.nr_lines
all_v_source_bytes += pf.nr_bytes
}
mut sall_v_source_lines := all_v_source_lines.str()
mut sall_v_source_bytes := all_v_source_bytes.str()
sall_v_source_lines = util.bold('${sall_v_source_lines:10s}')
sall_v_source_bytes = util.bold('${sall_v_source_bytes:10s}')
println(' V source code size: $sall_v_source_lines lines, $sall_v_source_bytes bytes')
//
mut slines := b.stats_lines.str()
mut sbytes := b.stats_bytes.str()
slines = util.bold('${slines:10s}')
sbytes = util.bold('${sbytes:10s}')
println('generated target code size: $slines lines, $sbytes bytes')
//
vlines_per_second := int(1_000_000.0 * f64(all_v_source_lines) / f64(compilation_time_micros))
svlines_per_second := util.bold(vlines_per_second.str())
println('compilation took: $scompilation_time_ms ms, compilation speed: $svlines_per_second vlines/s')
}
b.exit_on_invalid_syntax()
// running does not require the parsers anymore
unsafe { b.myfree() }
if pref.is_test || pref.is_run {
b.run_compiled_executable_and_exit()
}
}
pub fn (mut b Builder) get_vtmp_filename(base_file_name string, postfix string) string {
vtmp := util.get_vtmp_folder()
mut uniq := ''
if !b.pref.reuse_tmpc {
uniq = '.$rand.u64()'
}
fname := os.file_name(os.real_path(base_file_name)) + '$uniq$postfix'
return os.real_path(os.join_path(vtmp, fname))
} }
// Temporary, will be done by -autofree // Temporary, will be done by -autofree
@ -118,9 +79,9 @@ fn (mut b Builder) run_compiled_executable_and_exit() {
if b.pref.os == .ios { if b.pref.os == .ios {
panic('Running iOS apps is not supported yet.') panic('Running iOS apps is not supported yet.')
} }
if b.pref.is_verbose { if !(b.pref.is_test || b.pref.is_run || b.pref.is_crun) {
exit(0)
} }
if b.pref.is_test || b.pref.is_run {
compiled_file := os.real_path(b.pref.out_name) compiled_file := os.real_path(b.pref.out_name)
run_file := if b.pref.backend.is_js() { run_file := if b.pref.backend.is_js() {
node_basename := $if windows { 'node.exe' } $else { 'node' } node_basename := $if windows { 'node.exe' } $else { 'node' }
@ -158,8 +119,6 @@ fn (mut b Builder) run_compiled_executable_and_exit() {
b.cleanup_run_executable_after_exit(compiled_file) b.cleanup_run_executable_after_exit(compiled_file)
exit(ret) exit(ret)
} }
exit(0)
}
fn eshcb(_ os.Signal) { fn eshcb(_ os.Signal) {
} }
@ -171,6 +130,9 @@ fn serror(reason string, e IError) {
} }
fn (mut v Builder) cleanup_run_executable_after_exit(exefile string) { fn (mut v Builder) cleanup_run_executable_after_exit(exefile string) {
if v.pref.is_crun {
return
}
if v.pref.reuse_tmpc { if v.pref.reuse_tmpc {
v.pref.vrun_elog('keeping executable: $exefile , because -keepc was passed') v.pref.vrun_elog('keeping executable: $exefile , because -keepc was passed')
return return

View File

@ -2,6 +2,8 @@ module builder
import os import os
import hash import hash
import time
import rand
import strings import strings
import v.util import v.util
import v.pref import v.pref
@ -11,11 +13,27 @@ pub fn (mut b Builder) rebuild_modules() {
if !b.pref.use_cache || b.pref.build_mode == .build_module { if !b.pref.use_cache || b.pref.build_mode == .build_module {
return return
} }
all_files := b.parsed_files.map(it.path)
$if trace_invalidations ? {
eprintln('> rebuild_modules all_files: $all_files')
}
invalidations := b.find_invalidated_modules_by_files(all_files)
$if trace_invalidations ? {
eprintln('> rebuild_modules invalidations: $invalidations')
}
if invalidations.len > 0 {
vexe := pref.vexe_path()
for imp in invalidations {
b.v_build_module(vexe, imp)
}
}
}
pub fn (mut b Builder) find_invalidated_modules_by_files(all_files []string) []string {
util.timing_start('${@METHOD} source_hashing') util.timing_start('${@METHOD} source_hashing')
mut new_hashes := map[string]string{} mut new_hashes := map[string]string{}
mut old_hashes := map[string]string{} mut old_hashes := map[string]string{}
mut sb_new_hashes := strings.new_builder(1024) mut sb_new_hashes := strings.new_builder(1024)
all_files := b.parsed_files.map(it.path)
// //
mut cm := vcache.new_cache_manager(all_files) mut cm := vcache.new_cache_manager(all_files)
sold_hashes := cm.load('.hashes', 'all_files') or { ' ' } sold_hashes := cm.load('.hashes', 'all_files') or { ' ' }
@ -31,8 +49,7 @@ pub fn (mut b Builder) rebuild_modules() {
old_hashes[cpath] = chash old_hashes[cpath] = chash
} }
// eprintln('old_hashes: $old_hashes') // eprintln('old_hashes: $old_hashes')
for p in b.parsed_files { for cpath in all_files {
cpath := p.path
ccontent := util.read_file(cpath) or { '' } ccontent := util.read_file(cpath) or { '' }
chash := hash.sum64_string(ccontent, 7).hex_full() chash := hash.sum64_string(ccontent, 7).hex_full()
new_hashes[cpath] = chash new_hashes[cpath] = chash
@ -48,6 +65,7 @@ pub fn (mut b Builder) rebuild_modules() {
cm.save('.hashes', 'all_files', snew_hashes) or {} cm.save('.hashes', 'all_files', snew_hashes) or {}
util.timing_measure('${@METHOD} source_hashing') util.timing_measure('${@METHOD} source_hashing')
mut invalidations := []string{}
if new_hashes != old_hashes { if new_hashes != old_hashes {
util.timing_start('${@METHOD} rebuilding') util.timing_start('${@METHOD} rebuilding')
// eprintln('> b.mod_invalidates_paths: $b.mod_invalidates_paths') // eprintln('> b.mod_invalidates_paths: $b.mod_invalidates_paths')
@ -148,13 +166,13 @@ pub fn (mut b Builder) rebuild_modules() {
} }
if invalidated_mod_paths.len > 0 { if invalidated_mod_paths.len > 0 {
impaths := invalidated_mod_paths.keys() impaths := invalidated_mod_paths.keys()
vexe := pref.vexe_path()
for imp in impaths { for imp in impaths {
b.v_build_module(vexe, imp) invalidations << imp
} }
} }
util.timing_measure('${@METHOD} rebuilding') util.timing_measure('${@METHOD} rebuilding')
} }
return invalidations
} }
fn (mut b Builder) v_build_module(vexe string, imp_path string) { fn (mut b Builder) v_build_module(vexe string, imp_path string) {
@ -237,3 +255,114 @@ fn (mut b Builder) handle_usecache(vexe string) {
} }
b.ccoptions.post_args << libs b.ccoptions.post_args << libs
} }
pub fn (mut b Builder) should_rebuild() bool {
mut exe_name := b.pref.out_name
$if windows {
exe_name = exe_name + '.exe'
}
if !os.is_file(exe_name) {
return true
}
if !b.pref.is_crun {
return true
}
mut v_program_files := []string{}
is_file := os.is_file(b.pref.path)
is_dir := os.is_dir(b.pref.path)
if is_file {
v_program_files << b.pref.path
} else if is_dir {
v_program_files << b.v_files_from_dir(b.pref.path)
}
v_program_files.sort() // ensure stable keys for the dependencies cache
b.crun_cache_keys = v_program_files
b.crun_cache_keys << exe_name
// just check the timestamps for now:
exe_stamp := os.file_last_mod_unix(exe_name)
source_stamp := most_recent_timestamp(v_program_files)
if exe_stamp <= source_stamp {
return true
}
////////////////////////////////////////////////////////////////////////////
// The timestamps for the top level files were found ok,
// however we want to *also* make sure that a full rebuild will be done
// if any of the dependencies (if we know them) are changed.
mut cm := vcache.new_cache_manager(b.crun_cache_keys)
// always rebuild, when the compilation options changed between 2 sequential cruns:
sbuild_options := cm.load('.build_options', '.crun') or { return true }
if sbuild_options != b.pref.build_options.join('\n') {
return true
}
sdependencies := cm.load('.dependencies', '.crun') or {
// empty/wiped out cache, we do not know what the dependencies are, so just
// rebuild, which will fill in the dependencies cache for the next crun
return true
}
dependencies := sdependencies.split('\n')
// we have already compiled these source files, and have their dependencies
dependencies_stamp := most_recent_timestamp(dependencies)
if dependencies_stamp < exe_stamp {
return false
}
return true
}
fn most_recent_timestamp(files []string) i64 {
mut res := i64(0)
for f in files {
f_stamp := os.file_last_mod_unix(f)
if res <= f_stamp {
res = f_stamp
}
}
return res
}
pub fn (mut b Builder) rebuild(backend_cb FnBackend) {
mut sw := time.new_stopwatch()
backend_cb(mut b)
if b.pref.is_crun {
// save the dependencies after the first compilation, they will be used for subsequent ones:
mut cm := vcache.new_cache_manager(b.crun_cache_keys)
dependency_files := b.parsed_files.map(it.path)
cm.save('.dependencies', '.crun', dependency_files.join('\n')) or {}
cm.save('.build_options', '.crun', b.pref.build_options.join('\n')) or {}
}
mut timers := util.get_timers()
timers.show_remaining()
if b.pref.is_stats {
compilation_time_micros := 1 + sw.elapsed().microseconds()
scompilation_time_ms := util.bold('${f64(compilation_time_micros) / 1000.0:6.3f}')
mut all_v_source_lines, mut all_v_source_bytes := 0, 0
for pf in b.parsed_files {
all_v_source_lines += pf.nr_lines
all_v_source_bytes += pf.nr_bytes
}
mut sall_v_source_lines := all_v_source_lines.str()
mut sall_v_source_bytes := all_v_source_bytes.str()
sall_v_source_lines = util.bold('${sall_v_source_lines:10s}')
sall_v_source_bytes = util.bold('${sall_v_source_bytes:10s}')
println(' V source code size: $sall_v_source_lines lines, $sall_v_source_bytes bytes')
//
mut slines := b.stats_lines.str()
mut sbytes := b.stats_bytes.str()
slines = util.bold('${slines:10s}')
sbytes = util.bold('${sbytes:10s}')
println('generated target code size: $slines lines, $sbytes bytes')
//
vlines_per_second := int(1_000_000.0 * f64(all_v_source_lines) / f64(compilation_time_micros))
svlines_per_second := util.bold(vlines_per_second.str())
println('compilation took: $scompilation_time_ms ms, compilation speed: $svlines_per_second vlines/s')
}
}
pub fn (mut b Builder) get_vtmp_filename(base_file_name string, postfix string) string {
vtmp := util.get_vtmp_folder()
mut uniq := ''
if !b.pref.reuse_tmpc {
uniq = '.$rand.u64()'
}
fname := os.file_name(os.real_path(base_file_name)) + '$uniq$postfix'
return os.real_path(os.join_path(vtmp, fname))
}

View File

@ -111,7 +111,8 @@ pub mut:
is_prof bool // benchmark every function is_prof bool // benchmark every function
is_prod bool // use "-O2" is_prod bool // use "-O2"
is_repl bool is_repl bool
is_run bool is_run bool // compile and run a v program, passing arguments to it, and deleting the executable afterwards
is_crun bool // similar to run, but does not recompile the executable, if there were no changes to the sources
is_debug bool // turned on by -g or -cg, it tells v to pass -g to the C backend compiler. is_debug bool // turned on by -g or -cg, it tells v to pass -g to the C backend compiler.
is_vlines bool // turned on by -g (it slows down .tmp.c generation slightly). is_vlines bool // turned on by -g (it slows down .tmp.c generation slightly).
is_stats bool // `v -stats file_test.v` will produce more detailed statistics for the tests that were run is_stats bool // `v -stats file_test.v` will produce more detailed statistics for the tests that were run
@ -706,7 +707,7 @@ pub fn parse_args_and_show_errors(known_external_commands []string, args []strin
if command == '' { if command == '' {
command = arg command = arg
command_pos = i command_pos = i
if command == 'run' { if command in ['run', 'crun'] {
break break
} }
} else if is_source_file(command) && is_source_file(arg) } else if is_source_file(command) && is_source_file(arg)
@ -734,6 +735,12 @@ pub fn parse_args_and_show_errors(known_external_commands []string, args []strin
if res.is_debug { if res.is_debug {
res.parse_define('debug') res.parse_define('debug')
} }
if command == 'crun' {
res.is_crun = true
}
if command == 'run' {
res.is_run = true
}
if command == 'run' && res.is_prod && os.is_atty(1) > 0 { if command == 'run' && res.is_prod && os.is_atty(1) > 0 {
eprintln_cond(show_output, "Note: building an optimized binary takes much longer. It shouldn't be used with `v run`.") eprintln_cond(show_output, "Note: building an optimized binary takes much longer. It shouldn't be used with `v run`.")
eprintln_cond(show_output, 'Use `v run` without optimization, or build an optimized binary with -prod first, then run it separately.') eprintln_cond(show_output, 'Use `v run` without optimization, or build an optimized binary with -prod first, then run it separately.')
@ -744,8 +751,7 @@ pub fn parse_args_and_show_errors(known_external_commands []string, args []strin
eprintln('Cannot save output binary in a .v file.') eprintln('Cannot save output binary in a .v file.')
exit(1) exit(1)
} }
if command == 'run' { if res.is_run || res.is_crun {
res.is_run = true
if command_pos + 2 > args.len { if command_pos + 2 > args.len {
eprintln('v run: no v files listed') eprintln('v run: no v files listed')
exit(1) exit(1)
@ -798,7 +804,7 @@ pub fn parse_args_and_show_errors(known_external_commands []string, args []strin
// `v build.vsh gcc` is the same as `v run build.vsh gcc`, // `v build.vsh gcc` is the same as `v run build.vsh gcc`,
// i.e. compiling, then running the script, passing the args // i.e. compiling, then running the script, passing the args
// after it to the script: // after it to the script:
res.is_run = true res.is_crun = true
res.path = command res.path = command
res.run_args = args[command_pos + 1..] res.run_args = args[command_pos + 1..]
} else if command == 'interpret' { } else if command == 'interpret' {

View File

@ -0,0 +1,51 @@
import os
import time
const crun_folder = os.join_path(os.temp_dir(), 'crun_folder')
const vprogram_file = os.join_path(crun_folder, 'vprogram.vv')
const vexe = os.getenv('VEXE')
fn testsuite_begin() {
os.setenv('VCACHE', crun_folder, true)
os.rmdir_all(crun_folder) or {}
os.mkdir_all(crun_folder) or {}
assert os.is_dir(crun_folder)
}
fn testsuite_end() {
os.chdir(os.wd_at_startup) or {}
os.rmdir_all(crun_folder) or {}
assert !os.is_dir(crun_folder)
}
fn test_saving_simple_v_program() ? {
os.write_file(vprogram_file, 'print("hello")')?
assert true
}
fn test_crun_simple_v_program_several_times() ? {
mut sw := time.new_stopwatch()
mut times := []i64{}
for i in 0 .. 10 {
vcrun()
times << sw.elapsed().microseconds()
time.sleep(50 * time.millisecond)
sw.restart()
}
dump(times)
assert times.first() > times.last() * 5 // cruns compile just once, if the source file is not changed
$if !windows {
os.system('ls -la $crun_folder')
os.system('find $crun_folder')
}
}
fn vcrun() {
cmd := '${os.quoted_path(vexe)} crun ${os.quoted_path(vprogram_file)}'
eprintln('now: $time.now().format_ss_milli() | cmd: $cmd')
res := os.execute(cmd)
assert res.exit_code == 0
assert res.output == 'hello'
}