// Copyright (c) 2019 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 main import os import time const ( Version = '0.1.0' ) // TODO no caps enum BuildMode { // `v program.v' // Build user code only, and add pre-compiled vlib (`cc program.o builtin.o os.o...`) DEFAULT_MODE // `v -embed_vlib program.v` // vlib + user code in one file (slower compilation, but easier when working on vlib and cross-compiling) EMBED_VLIB // `v -lib ~/v/os` // build any module (generate os.o + os.vh) BUILD // TODO a better name would be smth like `.build_module` I think } fn vtmp_path() string { return os.home_dir() + '/.vlang/' } const ( SupportedPlatforms = ['windows', 'mac', 'linux'] TmpPath = vtmp_path() ) enum Os { MAC LINUX WINDOWS } enum Pass { // A very short pass that only looks at imports in the begginning of each file RUN_IMPORTS // First pass, only parses and saves declarations (fn signatures, consts, types). // Skips function bodies. // We need this because in V things can be used before they are declared. RUN_DECLS // Second pass, parses function bodies and generates C or machine code. RUN_MAIN } /* // TODO rename to: enum Pass { imports decls main } */ struct V { mut: build_mode BuildMode os Os // the OS to build for nofmt bool // disable vfmt out_name_c string // name of the temporary C file files []string // all V files that need to be parsed and compiled dir string // directory (or file) being compiled (TODO rename to path?) table *Table // table with types, vars, functions etc cgen *CGen // C code generator is_test bool // `v test string_test.v` is_script bool // single file mode (`v program.v`), `fn main(){}` can be skipped is_so bool is_live bool // for hot code reloading is_prof bool // benchmark every function translated bool // `v translated doom.v` are we running V code translated from C? allow globals, ++ expressions, etc obfuscate bool // `v -obf program.v`, renames functions to "f_XXX" lang_dir string // "~/code/v" is_verbose bool // print extra information with `v.log()` is_run bool // `v run program.v` is_play bool // playground mode show_c_cmd bool // `v -show_c_cmd` prints the C command to build program.v.c sanitize bool // use Clang's new "-fsanitize" option out_name string // "program.exe" is_prod bool // use "-O2" and skip printlns (TODO I don't thik many people want printlns to disappear in prod buidls) is_repl bool } fn main() { // There's no `flags` module yet, so args have to be parsed manually args := os.args // Print the version and exit. if 'version' in args { println('V $Version') return } if '-h' in args || '--help' in args || 'help' in args { println(HelpText) return } if 'translate' in args { println('Translating C to V will be available in V 0.3') return } // TODO quit if the compiler is too old // u := os.file_last_mod_unix('/var/tmp/alex') // Create a temp directory if it's not there. if !os.file_exists(TmpPath) { os.mkdir(TmpPath) } // If there's no tmp path with current version yet, the user must be using a pre-built package // Copy the `vlib` directory to the tmp path. /* // TODO if !os.file_exists(TmpPath) && os.file_exists('vlib') { } */ // Just fmt and exit if args.contains('fmt') { file := args.last() if !os.file_exists(file) { println('"$file" does not exist') exit(1) } if !file.ends_with('.v') { println('v fmt can only be used on .v files') exit(1) } println('vfmt is temporarily disabled') return } // V with no args? REPL if args.len < 2 { run_repl() return } // Construct the V object from command line arguments mut c := new_v(args) if c.is_verbose { println(args) } // Generate the docs and exit if args.contains('doc') { // c.gen_doc_html_for_module(args.last()) exit(0) } c.compile() } fn (c mut V) compile() { mut cgen := c.cgen cgen.genln('// Generated by V') // Add user files to compile c.add_user_v_files() if c.is_verbose { println('all .v files:') println(c.files) } // First pass (declarations) for file in c.files { mut p := c.new_parser(file, RUN_DECLS) p.parse() } // Main pass cgen.run = RUN_MAIN if c.is_play { cgen.genln('#define VPLAY (1) ') } cgen.genln(' #include // TODO remove all these includes, define all function signatures and types manually #include #include #include // for va_list #include // int64_t etc #ifdef __linux__ #include #endif #ifdef __APPLE__ #endif #ifdef _WIN32 #include //#include #endif //================================== TYPEDEFS ================================*/ typedef unsigned char byte; typedef unsigned int uint; typedef int64_t i64; typedef int32_t i32; typedef int16_t i16; typedef int8_t i8; typedef uint64_t u64; typedef uint32_t u32; typedef uint16_t u16; typedef uint8_t u8; typedef uint32_t rune; typedef float f32; typedef double f64; typedef unsigned char* byteptr; typedef int* intptr; typedef void* voidptr; typedef struct array array; typedef struct map map; typedef array array_string; typedef array array_int; typedef array array_byte; typedef array array_uint; typedef array array_float; typedef map map_int; typedef map map_string; #ifndef bool typedef int bool; #define true 1 #define false 0 #endif //============================== HELPER C MACROS =============================*/ #define _PUSH(arr, val, tmp, tmp_typ) {tmp_typ tmp = (val); array__push(arr, &tmp);} #define _IN(typ, val, arr) array_##typ##_contains(arr, val) #define ALLOC_INIT(type, ...) (type *)memdup((type[]){ __VA_ARGS__ }, sizeof(type)) #define UTF8_CHAR_LEN( byte ) (( 0xE5000000 >> (( byte >> 3 ) & 0x1e )) & 3 ) + 1 //================================== GLOBALS =================================*/ //int V_ZERO = 0; byteptr g_str_buf; int load_so(byteptr); void reload_so(); void init_consts();') imports_json := c.table.imports.contains('json') // TODO remove global UI hack if c.os == MAC && ((c.build_mode == EMBED_VLIB && c.table.imports.contains('ui')) || (c.build_mode == BUILD && c.dir.contains('/ui'))) { cgen.genln('id defaultFont = 0; // main.v') } // TODO remove ugly .c include once V has its own json parser // Embed cjson either in embedvlib or in json.o if imports_json && c.build_mode == EMBED_VLIB || (c.build_mode == BUILD && c.out_name.contains('json.o')) { cgen.genln('#include "cJSON.c" ') } // We need the cjson header for all the json decoding user will do in default mode if c.build_mode == DEFAULT_MODE { if imports_json { cgen.genln('#include "cJSON.h"') } } if c.build_mode == EMBED_VLIB || c.build_mode == DEFAULT_MODE { // If we declare these for all modes, then when running `v a.v` we'll get // `/usr/bin/ld: multiple definition of 'total_m'` cgen.genln('i64 total_m = 0; // For counting total RAM allocated') cgen.genln('int g_test_ok = 1; ') if c.table.imports.contains('json') { cgen.genln(' #define js_get(object, key) cJSON_GetObjectItemCaseSensitive((object), (key)) ') } } if os.args.contains('-debug_alloc') { cgen.genln('#define DEBUG_ALLOC 1') } cgen.genln('/*================================== FNS =================================*/') cgen.genln('this line will be replaced with definitions') defs_pos := cgen.lines.len - 1 for file in c.files { mut p := c.new_parser(file, RUN_MAIN) p.parse() // p.g.gen_x64() // Format all files (don't format automatically generated vlib headers) if !c.nofmt && !file.contains('/vlib/') { // new vfmt is not ready yet } } c.log('Done parsing.') // Write everything mut d := new_string_builder(10000)// Just to avoid some unnecessary allocations d.writeln(cgen.includes.join_lines()) d.writeln(cgen.typedefs.join_lines()) d.writeln(cgen.types.join_lines()) d.writeln('\nstring _STR(const char*, ...);\n') d.writeln('\nstring _STR_TMP(const char*, ...);\n') d.writeln(cgen.fns.join_lines()) d.writeln(cgen.consts.join_lines()) d.writeln(cgen.thread_args.join_lines()) if c.is_prof { d.writeln('; // Prof counters:') d.writeln(c.prof_counters()) } dd := d.str() cgen.lines.set(defs_pos, dd)// TODO `def.str()` doesn't compile // if c.build_mode in [.default, .embed_vlib] { if c.build_mode == DEFAULT_MODE || c.build_mode == EMBED_VLIB { // vlib can't have `init_consts()` cgen.genln('void init_consts() { g_str_buf=malloc(1000); ${cgen.consts_init.join_lines()} }') // _STR function can't be defined in vlib cgen.genln(' string _STR(const char *fmt, ...) { va_list argptr; va_start(argptr, fmt); size_t len = vsnprintf(0, 0, fmt, argptr) + 1; va_end(argptr); byte* buf = malloc(len); va_start(argptr, fmt); vsprintf(buf, fmt, argptr); va_end(argptr); #ifdef DEBUG_ALLOC puts("_STR:"); puts(buf); #endif return tos2(buf); } string _STR_TMP(const char *fmt, ...) { va_list argptr; va_start(argptr, fmt); size_t len = vsnprintf(0, 0, fmt, argptr) + 1; va_end(argptr); va_start(argptr, fmt); vsprintf(g_str_buf, fmt, argptr); va_end(argptr); #ifdef DEBUG_ALLOC //puts("_STR_TMP:"); //puts(g_str_buf); #endif return tos2(g_str_buf); } ') } // Make sure the main function exists // Obviously we don't need it in libraries if c.build_mode != BUILD { if !c.table.main_exists() && !c.is_test { // It can be skipped in single file programs if c.is_script { println('Generating main()...') cgen.genln('int main() { $cgen.fn_main; return 0; }') } else { println('panic: function `main` is undeclared in the main module') } } // Generate `main` which calls every single test function else if c.is_test { cgen.genln('int main() { init_consts();') for v in c.table.fns { if v.name.starts_with('test_') { cgen.genln('$v.name();') } } cgen.genln('return g_test_ok == 0; }') } } if c.is_live { cgen.genln(' int load_so(byteptr path) { printf("load_so %s\\n", path); dlclose(live_lib); live_lib = dlopen(path, RTLD_LAZY); if (!live_lib) {puts("open failed"); exit(1); return 0;} ') for so_fn in cgen.so_fns { cgen.genln('$so_fn = dlsym(live_lib, "$so_fn"); ') } cgen.genln('return 1; }') } cgen.save() c.log('flags=') if c.is_verbose { println(c.table.flags) } c.cc() if c.is_test || c.is_run { if true || c.is_verbose { println('============running $c.out_name==============================') } cmd := if c.out_name.starts_with('/') { c.out_name } else { './' + c.out_name } ret := os.system2(cmd) if ret != 0 { s := os.system(cmd) println(s) println('ret not 0, exiting') exit(1) } } } fn (c mut V) cc() { linux_host := os.user_os() == 'linux' c.log('cc() isprod=$c.is_prod outname=$c.out_name') mut a := ['-w']// arguments for the C compiler flags := c.table.flags.join(' ') /* mut shared := '' if c.is_so { a << '-shared'// -Wl,-z,defs' c.out_name = c.out_name + '.so' } */ if c.is_prod { a << '-O2' } else { a << '-g' } mut libs := ''// builtin.o os.o http.o etc if c.build_mode == BUILD { a << '-c' } else if c.build_mode == EMBED_VLIB { // } else if c.build_mode == DEFAULT_MODE { libs = '$TmpPath/vlib/builtin.o' if !os.file_exists(libs) { println('`builtin.o` not found') exit(1) } for imp in c.table.imports { if imp == 'webview' { continue } libs += ' $TmpPath/vlib/${imp}.o' } } // -I flags /* mut args := '' for flag in c.table.flags { if !flag.starts_with('-l') { args += flag args += ' ' } } */ if c.sanitize { a << '-fsanitize=leak' } // Cross compiling linux sysroot := '/Users/alex/tmp/lld/linuxroot/' if c.os == LINUX && !linux_host { // Build file.o a << '-c --sysroot=$sysroot -target x86_64-linux-gnu' // Right now `out_name` can be `file`, not `file.o` if !c.out_name.ends_with('.o') { c.out_name = c.out_name + '.o' } } // Cross compiling windows // sysroot := '/Users/alex/tmp/lld/linuxroot/' // Output executable name // else { a << '-o $c.out_name' // The C file we are compiling a << '$TmpPath/$c.out_name_c' // } // Min macos version is mandatory I think? if c.os == MAC { a << '-mmacosx-version-min=10.7' } a << flags a << libs // macOS code can include objective C TODO remove once objective C is replaced with C if c.os == MAC { a << '-x objective-c' } // Without these libs compilation will fail on Linux if c.os == LINUX && c.build_mode != BUILD { a << '-lm -ldl -lpthread' } // Find clang executable fast_clang := '/usr/local/Cellar/llvm/8.0.0/bin/clang' args := a.join(' ') cmd := if os.file_exists(fast_clang) { '$fast_clang -I. $args' } else { 'cc -I. $args' } // Print the C command if c.show_c_cmd || c.is_verbose { println('\n==========\n$cmd\n=========\n') } // Run res := os.system(cmd) // println('C OUTPUT:') if res.contains('error: ') { println(res) panic('clang error') } // Link it if we are cross compiling and need an executable if c.os == LINUX && !linux_host && c.build_mode != BUILD { c.out_name = c.out_name.replace('.o', '') obj_file := c.out_name + '.o' println('linux obj_file=$obj_file out_name=$c.out_name') ress := os.system('/usr/local/Cellar/llvm/8.0.0/bin/ld.lld --sysroot=$sysroot ' + '-v -o $c.out_name ' + '-m elf_x86_64 -dynamic-linker /lib64/ld-linux-x86-64.so.2 ' + '/usr/lib/x86_64-linux-gnu/crt1.o ' + '$sysroot/lib/x86_64-linux-gnu/libm-2.28.a ' + '/usr/lib/x86_64-linux-gnu/crti.o ' + obj_file + ' /usr/lib/x86_64-linux-gnu/libc.so ' + '/usr/lib/x86_64-linux-gnu/crtn.o') println(ress) if ress.contains('error:') { os.exit(1) } println('linux cross compilation done. resulting binary: "$c.out_name"') } // print_time('after gcc') } fn (c &V) v_files_from_dir(dir string) []string { mut res := []string if !os.file_exists(dir) { panic('$dir doesn\'t exist') } else if !os.dir_exists(dir) { panic('$dir isn\'t a directory') } mut files := os.ls(dir) if c.is_verbose { println('v_files_from_dir ("$dir")') } // println(files.len) // println(files) files.sort() for file in files { c.log('F=$file') if !file.ends_with('.v') && !file.ends_with('.vh') { continue } if file.ends_with('_test.v') { continue } if file.ends_with('_win.v') && c.os != WINDOWS { continue } if file.ends_with('_lin.v') && c.os != LINUX { continue } if file.ends_with('_mac.v') && c.os != MAC { lin_file := file.replace('_mac.v', '_lin.v') // println('lin_file="$lin_file"') // If there are both _mac.v and _lin.v, don't use _mac.v if os.file_exists('$dir/$lin_file') { continue } else if c.os == WINDOWS { continue } else { // If there's only _mac.v, then it can be used on Linux too } } res << '$dir/$file' } return res } // Parses imports, adds necessary libs, and then user files fn (c mut V) add_user_v_files() { mut dir := c.dir c.log('add_v_files($dir)') // Need to store user files separately, because they have to be added after libs, but we dont know // which libs need to be added yet mut user_files := []string // v volt/slack_test.v: compile all .v files to get the environment // I need to implement user packages! TODO is_test_with_imports := dir.ends_with('_test.v') && (dir.contains('/volt') || dir.contains('/c2volt'))// TODO if is_test_with_imports { user_files << dir pos := dir.last_index('/') dir = dir.left(pos) + '/'// TODO WHY IS THIS NEEDED? } if dir.ends_with('.v') { // Just compile one file and get parent dir user_files << dir dir = dir.all_before('/') } else { // Add files from the dir user is compiling (only .v files) files := c.v_files_from_dir(dir) for file in files { user_files << file } } if user_files.len == 0 { println('No input .v files') exit(1) } if c.is_verbose { c.log('user_files:') println(user_files) } // Parse user imports for file in user_files { mut p := c.new_parser(file, RUN_IMPORTS) p.parse() } // Parse lib imports if c.build_mode == DEFAULT_MODE { for i := 0; i < c.table.imports.len; i++ { pkg := c.table.imports[i] vfiles := c.v_files_from_dir('$TmpPath/vlib/$pkg') // Add all imports referenced by these libs for file in vfiles { mut p := c.new_parser(file, RUN_IMPORTS) p.parse() } } } else { // TODO this used to crash compiler? // for pkg in c.table.imports { for i := 0; i < c.table.imports.len; i++ { pkg := c.table.imports[i] // mut import_path := '$c.lang_dir/$pkg' vfiles := c.v_files_from_dir('$c.lang_dir/$pkg') // Add all imports referenced by these libs for file in vfiles { mut p := c.new_parser(file, RUN_IMPORTS) p.parse() } } } if c.is_verbose { c.log('imports:') println(c.table.imports) } // Only now add all combined lib files for pkg in c.table.imports { mut module_path := '$c.lang_dir/$pkg' // If we are in default mode, we don't parse vlib .v files, but header .vh files in // TmpPath/vlib // These were generated by vfmt if c.build_mode == DEFAULT_MODE || c.build_mode == BUILD { module_path = '$TmpPath/vlib/$pkg' } vfiles := c.v_files_from_dir(module_path) for vfile in vfiles { c.files << vfile } // TODO c.files.append_array(vfiles) } // Add user code last for file in user_files { c.files << file } // c.files.append_array(user_files) } fn get_arg(joined_args, arg, def string) string { key := '-$arg ' mut pos := joined_args.index(key) if pos == -1 { return def } pos += key.len mut space := joined_args.index_after(' ', pos) if space == -1 { space = joined_args.len } res := joined_args.substr(pos, space) // println('get_arg($arg) = "$res"') return res } fn (c &V) log(s string) { if !c.is_verbose { return } println(s) } fn new_v(args[]string) *V { mut dir := args.last() // println('new compiler "$dir"') if args.len < 2 { dir = '' } joined_args := args.join(' ') target_os := get_arg(joined_args, 'os', '') mut out_name := get_arg(joined_args, 'o', 'a.out') // build mode mut build_mode := DEFAULT_MODE if args.contains('-lib') { build_mode = BUILD // v -lib ~/v/os => os.o base := dir.all_after('/') println('Building module ${base}...') out_name = '$TmpPath/vlib/${base}.o' // Cross compiling? Use separate dirs for each os if target_os != os.user_os() { os.mkdir('$TmpPath/vlib/$target_os') out_name = '$TmpPath/vlib/$target_os/${base}.o' println('Cross compiling $out_name') } } // TODO embed_vlib is temporarily the default mode. It's much slower. else if !args.contains('-embed_vlib') { build_mode = EMBED_VLIB } // is_test := dir.ends_with('_test.v') is_script := dir.ends_with('.v') if is_script && !os.file_exists(dir) { println('`$dir` does not exist') exit(1) } // No -o provided? foo.v => foo if out_name == 'a.out' && dir.ends_with('.v') { out_name = dir.left(dir.len - 2) } // if we are in `/foo` and run `v .`, the executable should be `foo` if dir == '.' && out_name == 'a.out' { base := os.getwd().all_after('/') out_name = base.trim_space() } mut _os := MAC // No OS specifed? Use current system if target_os == '' { $if linux { _os = LINUX } $if mac { _os = MAC } $if windows { _os = WINDOWS } } else { switch target_os { case 'linux': _os = LINUX case 'windows': _os = WINDOWS case 'mac': _os = MAC } } builtins := [ 'array.v', 'string.v', 'builtin.v', 'int.v', 'utf8.v', 'map.v', 'smap.v', 'option.v', 'string_builder.v', ] // Location of all vlib files TODO allow custom location mut lang_dir = os.home_dir() + '/code/v/' if !os.dir_exists(lang_dir) { println('$lang_dir not found. Run:') println('git clone https://github.com/vlang/v ~/code/v') exit(1) } out_name_c := out_name.all_after('/') + '.c' mut files := []string // Add builtin files if !out_name.contains('builtin.o') { for builtin in builtins { mut f := '$lang_dir/builtin/$builtin' // In default mode we use precompiled vlib.o, point to .vh files with signatures if build_mode == DEFAULT_MODE || build_mode == BUILD { f = '$TmpPath/vlib/builtin/${builtin}h' } files << f } } obfuscate := args.contains('-obf') return &V { os: _os out_name: out_name files: files dir: dir lang_dir: lang_dir table: new_table(obfuscate) out_name: out_name out_name_c: out_name_c is_test: is_test is_script: is_script is_so: args.contains('-shared') is_play: args.contains('play') is_prod: args.contains('-prod') is_verbose: args.contains('-verbose') obfuscate: obfuscate is_prof: args.contains('-prof') is_live: args.contains('-live') sanitize: args.contains('-sanitize') nofmt: args.contains('-nofmt') show_c_cmd: args.contains('-show_c_cmd') translated: args.contains('translated') cgen: new_cgen(out_name_c) build_mode: build_mode is_run: args.contains('run') is_repl: args.contains('-repl') } } fn run_repl() []string { println('V $Version') println('Use Ctrl-D to exit') println('For now you have to use println() to print values, this will be fixed soon\n') file := TmpPath + '/vrepl.v' mut lines := []string for { print('>>> ') mut line := os.get_line().trim_space() if line == '' { break } // Save the source only if the user is printing something, // but don't add this print call to the `lines` array, // so that it doesn't get called during the next print. if line.starts_with('print') { // TODO remove this once files without main compile correctly void_line := line.substr(line.index('(') + 1, line.len - 1) lines << void_line source_code := 'fn main(){' + lines.join('\n') + '\n' + line + '}' os.write_file(file, source_code) mut v := new_v( ['v', '-repl', file]) v.compile() s := os.system(TmpPath + '/vrepl') println(s) } else { lines << line } } return lines } // This definitely needs to be better :) const ( HelpText = ' - Build a V program: v file.v - Get current V version: v version - Build an optimized executable: v -prod file.v - Specify the executable\'s name: v -o program file.v - Build and execute a V program: v run file.v - Obfuscate the resulting binary: v -obf -prod build file.v - Test: v string_test.v ' ) /* - To disable automatic formatting: v -nofmt file.v - To build a program with an embedded vlib (use this if you do not have prebuilt vlib libraries or if you are working on vlib) v -embed_vlib file.v */