diff --git a/cmd/v/macos_v3_args.c.v b/cmd/v/macos_v3_args.c.v index 41edda4b449eed..57517ef2dbb674 100644 --- a/cmd/v/macos_v3_args.c.v +++ b/cmd/v/macos_v3_args.c.v @@ -43,10 +43,12 @@ fn is_macos_v3_compiler_bootstrap(normalized_path string) bool { // macos_v3_force_requested reports whether `-new-compiler` should hand this // invocation to the embedded V3 compiler. It gates on `-old-compiler` // precedence, options/modes V3 cannot honor yet, and whether the command is an -// actual compilation command (never `test`, external tools, or the `cmd/v` / -// `vlib/v3/v3.v` bootstrap). Both the Darwin dispatcher (where it overrides the -// default heuristic) and the non-macOS dispatcher (where it is the sole gate) -// rely on it, so it must stay platform neutral. +// actual compilation command (never `test` or external tools). Compiler bootstrap +// targets normally stay on V1, but explicit `-b fastc` still routes to V3 so its +// AST-free parser can report unsupported source instead of silently selecting V1. +// Both the Darwin dispatcher (where it overrides the default heuristic) and the +// non-macOS dispatcher (where it is the sole gate) rely on it, so it must stay +// platform neutral. @[markused] fn macos_v3_force_requested(command string, prefs &pref.Preferences) bool { if !prefs.new_compiler || prefs.old_compiler { @@ -55,7 +57,9 @@ fn macos_v3_force_requested(command string, prefs &pref.Preferences) bool { if v3_has_v1_only_preferences(prefs) || (prefs.gc_set_by_flag && prefs.gc_mode != .no_gc) { return false } - if prefs.autofree && prefs.is_run { + if prefs.autofree && prefs.is_run && !macos_v3_fastc_requested(prefs) { + // V1 still owns the established `v -autofree run ...` orchestration. + // Explicit FastC stays on V3 and reports this mode as unsupported. return false } if prefs.path == '' || command == 'test' || macos_v3_non_compilation_command(command) @@ -63,15 +67,45 @@ fn macos_v3_force_requested(command string, prefs &pref.Preferences) bool { return false } normalized_path := prefs.path.replace('\\', '/').trim_right('/') - if normalized_path == 'cmd/v' || normalized_path.starts_with('cmd/v/') + compiler_bootstrap := normalized_path == 'cmd/v' || normalized_path.starts_with('cmd/v/') || normalized_path.contains('/cmd/v/') || normalized_path.ends_with('/cmd/v') - || is_macos_v3_compiler_bootstrap(normalized_path) { + || is_macos_v3_compiler_bootstrap(normalized_path) + if compiler_bootstrap && !macos_v3_fastc_requested(prefs) { return false } return command in ['run', 'build'] || prefs.is_script || os.is_dir(prefs.path) || normalized_path.ends_with('.v') || normalized_path.ends_with('.vsh') } +fn macos_v3_fastc_requested(prefs &pref.Preferences) bool { + return prefs.is_fastc +} + +// macos_v3_fastc_incompatibility reports why an explicit FastC selection cannot +// be honored, so dispatchers fail instead of silently continuing through V1. +fn macos_v3_fastc_incompatibility(prefs &pref.Preferences) ?string { + if !prefs.is_fastc { + return none + } + if prefs.gc_set_by_flag && prefs.gc_mode != .no_gc { + return '`-b fastc` only supports `-gc none`; remove the explicit collector or select `-b c`.' + } + if v3_has_v1_only_preferences(prefs) { + return '`-b fastc` cannot be combined with an option that is only supported by the V1 compiler.' + } + return none +} + +// macos_v3_test_ownership_uses_v1 keeps ownership/autofree test binaries on +// V1. vtest marks its per-file compilations with `-skip-running`; compiling an +// autofree test through the ownership-enabled V3 tool can consume far more +// memory than the test itself. Explicit FastC is never diverted to an AST-based +// ownership compiler; its parser reports the unsupported mode itself. +fn macos_v3_test_ownership_uses_v1(prefs &pref.Preferences, args []string) bool { + return prefs.skip_running && !macos_v3_fastc_requested(prefs) + && (prefs.autofree || '-ownership' in args) +} + // These helpers are shared by the native Darwin dispatcher and the default // implementation selected while generating cross-platform VC sources, so this // file has to stay platform neutral (no `_darwin.c.v` suffix). Keep them outside diff --git a/cmd/v/macos_v3_darwin.c.v b/cmd/v/macos_v3_darwin.c.v index 37b67b46431636..e3324290b29e0d 100644 --- a/cmd/v/macos_v3_darwin.c.v +++ b/cmd/v/macos_v3_darwin.c.v @@ -37,9 +37,17 @@ fn maybe_delegate_to_macos_v3(command string, prefs &pref.Preferences) ?MacosV3C } return take_macos_v3_c_error_report() } + if message := macos_v3_fastc_incompatibility(prefs) { + eprintln(message) + exit(1) + } all_args := util.join_env_vflags_and_os_args() forwarded_args := all_args[1..] - if !is_macos_v3_default_executable(os.executable()) { + if macos_v3_test_ownership_uses_v1(prefs, forwarded_args) { + trace_macos_v3_skip('vtest ownership/autofree compilation') + return take_macos_v3_c_error_report() + } + if !macos_v3_executable_can_dispatch(os.executable(), prefs) { trace_macos_v3_skip('non-default compiler executable `${os.executable()}`') return none } @@ -66,6 +74,10 @@ fn is_macos_v3_default_executable(vexe string) bool { return os.base(vexe) in ['v', 'v.exe', 'vnew', 'vnew.exe'] } +fn macos_v3_executable_can_dispatch(vexe string, prefs &pref.Preferences) bool { + return is_macos_v3_default_executable(vexe) || macos_v3_fastc_requested(prefs) +} + fn is_macos_v3_relevant_command(command string, prefs &pref.Preferences) bool { if prefs.old_compiler { return false @@ -76,9 +88,10 @@ fn is_macos_v3_relevant_command(command string, prefs &pref.Preferences) bool { // dispatch, but it must not prevent V3 from being the default compiler. return false } - if prefs.autofree && prefs.is_run { + if prefs.autofree && prefs.is_run && !macos_v3_fastc_requested(prefs) { // V1 still owns the established `v -autofree run ...` orchestration. // Direct autofree builds are selected earlier by the ownership dispatcher. + // Explicit FastC stays on V3 and reports this mode as unsupported. return false } if command == 'test' { diff --git a/cmd/v/macos_v3_default.c.v b/cmd/v/macos_v3_default.c.v index c00810b40de21b..cdc9be8034558c 100644 --- a/cmd/v/macos_v3_default.c.v +++ b/cmd/v/macos_v3_default.c.v @@ -8,8 +8,8 @@ import v.util // compiler normally. `-new-compiler` opts into the embedded V3 driver and runs // it in THIS process, the same way V3 runs by default on macOS. It applies the // same gating as the Darwin dispatcher: `-old-compiler` wins, V1-only options -// are rejected, and only actual compilation commands are taken over (never -// `fmt`, `version`, `test`, external tools, or the compiler bootstrap). +// are rejected, and only actual compilation commands are taken over. Compiler +// bootstrap targets are included only when the V3-only fastc backend is explicit. fn maybe_delegate_to_macos_v3(command string, prefs &pref.Preferences) ?MacosV3CErrorReport { if !prefs.new_compiler || prefs.old_compiler { // V1 is the default here, and `-old-compiler` takes precedence. @@ -19,6 +19,10 @@ fn maybe_delegate_to_macos_v3(command string, prefs &pref.Preferences) ?MacosV3C eprintln('`-new-compiler` requires a build that embeds the V3 compiler, which this one does not.') exit(1) } + if message := macos_v3_fastc_incompatibility(prefs) { + eprintln(message) + exit(1) + } raw_args := util.join_env_vflags_and_os_args()[1..] if macos_v3_has_v1_only_leading_option(raw_args, command) { eprintln('`-new-compiler` cannot be combined with a V1-only option; drop `-new-compiler` or the option.') diff --git a/cmd/v/macos_v3_test.v b/cmd/v/macos_v3_test.v index 9ceca74f5b3819..70dcd449808046 100644 --- a/cmd/v/macos_v3_test.v +++ b/cmd/v/macos_v3_test.v @@ -1615,6 +1615,11 @@ fn test_macos_v3_default_executable_excludes_temporary_self_hosted_compilers() { assert !is_macos_v3_default_executable('/tmp/v2') assert !is_macos_v3_default_executable('/tmp/vstrict1') assert !is_macos_v3_default_executable('/tmp/vp') + assert !macos_v3_executable_can_dispatch('/tmp/v2', &pref.Preferences{}) + assert macos_v3_executable_can_dispatch('/tmp/v2', &pref.Preferences{ + build_options: ['-b fastc'] + is_fastc: true + }) } } @@ -1729,3 +1734,91 @@ fn test_macos_v3_new_compiler_routing_and_precedence() { path: 'main.v' }) } + +fn test_macos_v3_fastc_routes_compiler_selfhost_targets() { + for target in ['cmd/v', 'cmd/v/v.v', 'vlib/v3/v3.v'] { + assert macos_v3_force_requested('build', &pref.Preferences{ + new_compiler: true + path: target + build_options: ['-b fastc'] + is_fastc: true + }) + assert !macos_v3_force_requested('build', &pref.Preferences{ + new_compiler: true + path: target + build_options: ['-b c'] + }) + } + assert macos_v3_fastc_requested(&pref.Preferences{ + build_options: ['-backend fastc'] + is_fastc: true + }) + assert !macos_v3_fastc_requested(&pref.Preferences{ + build_options: ['-b fastc', '-b c'] + }) + repeated_backends, _ := pref.parse_args_and_show_errors([], ['-b', 'fastc', '-b', 'c', '-b', + 'fastc', 'cmd/v'], false) + assert macos_v3_fastc_requested(repeated_backends) + assert macos_v3_force_requested('run', &pref.Preferences{ + new_compiler: true + autofree: true + is_run: true + path: 'main.v' + build_options: ['-b fastc'] + is_fastc: true + }) +} + +fn test_macos_v3_fastc_rejects_incompatible_preferences() { + fastc_boehm, _ := pref.parse_args_and_show_errors([], + ['-b', 'fastc', '-gc', 'boehm', 'main.v'], false) + message := macos_v3_fastc_incompatibility(fastc_boehm) or { + assert false, 'expected explicit FastC with Boehm GC to be rejected' + return + } + assert message.contains('`-b fastc` only supports `-gc none`') + + overridden, _ := pref.parse_args_and_show_errors([], ['-b', 'fastc', '-gc', 'boehm', '-b', + 'c', 'main.v'], false) + assert macos_v3_fastc_incompatibility(overridden) == none +} + +fn test_macos_v3_fastc_allows_explicit_target_os() { + fastc_linux, _ := pref.parse_args_and_show_errors([], + ['-b', 'fastc', '-os', 'linux', 'main.v'], false) + assert fastc_linux.backend == .c + assert fastc_linux.is_fastc + assert fastc_linux.os == .linux + assert !v3_has_v1_only_preferences(fastc_linux) + assert macos_v3_fastc_incompatibility(fastc_linux) == none + assert macos_v3_force_requested('build', fastc_linux) + + fastc_cross_c, _ := pref.parse_args_and_show_errors([], ['-b', 'fastc', '-os', 'windows', '-o', + 'main.c', 'main.v'], false) + assert fastc_cross_c.backend == .c + assert fastc_cross_c.is_fastc + assert fastc_cross_c.os == .windows + assert fastc_cross_c.out_name.ends_with('main.c') + assert !v3_has_v1_only_preferences(fastc_cross_c) + assert macos_v3_fastc_incompatibility(fastc_cross_c) == none + assert macos_v3_force_requested('build', fastc_cross_c) + + standard_linux, _ := pref.parse_args_and_show_errors([], ['-b', 'c', '-os', 'linux', 'main.v'], + false) + assert !standard_linux.is_fastc + assert v3_has_v1_only_preferences(standard_linux) +} + +fn test_macos_v3_vtest_ownership_modes_use_v1_except_fastc() { + mut prefs := &pref.Preferences{ + skip_running: true + autofree: true + } + assert macos_v3_test_ownership_uses_v1(prefs, ['-skip-running', '-autofree', 'main.v']) + prefs.autofree = false + assert macos_v3_test_ownership_uses_v1(prefs, ['-skip-running', '-ownership', 'main.v']) + prefs.build_options = ['-b fastc'] + prefs.is_fastc = true + assert !macos_v3_test_ownership_uses_v1(prefs, ['-skip-running', '-ownership', '-b', 'fastc', + 'main.v']) +} diff --git a/cmd/v/v.v b/cmd/v/v.v index 7151595a6dfe05..1e7f945545440c 100644 --- a/cmd/v/v.v +++ b/cmd/v/v.v @@ -219,6 +219,16 @@ fn invoke_help_and_exit(remaining []string) { fn maybe_delegate_to_ownership(command string, prefs &pref.Preferences, merged_args []string) { is_ownership := '-ownership' in merged_args is_autofree := prefs.autofree + if prefs.is_fastc { + // FastC owns its whole invocation and must never launch the AST-based + // ownership compiler. Its direct parser reports unsupported modes. + return + } + $if macos { + if macos_v3_test_ownership_uses_v1(prefs, merged_args) { + return + } + } if !ownership_delegation_is_requested(is_ownership, is_autofree, prefs.old_compiler, os.user_os()) { return @@ -288,7 +298,7 @@ fn v3_has_v1_only_preferences(prefs &pref.Preferences) bool { || prefs.c_error_bug_report_url.len > 0 || prefs.wasm_validate || prefs.wasm_stack_top != 1024 + (16 * 1024) || prefs.line_info.len > 0 || prefs.use_coroutines || prefs.checker_match_exhaustive_cutoff_limit != 12 - || (prefs.backend == .c && prefs.os !in [._auto, .macos]) + || (prefs.backend == .c && !prefs.is_fastc && prefs.os !in [._auto, .macos]) || prefs.build_options.any(it.starts_with('-debug-tcc')) || prefs.is_musl || prefs.build_options.any(it in ['-musl', '-glibc']) || !prefs.relaxed_gcc14 { return true diff --git a/vlib/v/help/build/build.txt b/vlib/v/help/build/build.txt index daf5f8ce9a4e71..fe653be542a1c8 100644 --- a/vlib/v/help/build/build.txt +++ b/vlib/v/help/build/build.txt @@ -43,6 +43,7 @@ NB: the build flags are shared with the run command too: Specifies the backend that will be used for building the executable. Current list of supported backends: * `c` (default) - V outputs C source code, which is then passed to a C compiler. + * `fastc` - V parses supported source directly to C without an AST. * `go` - V outputs Go source code, which is then passed to a Go compiler. * `js` - V outputs JS source code which can be passed to NodeJS to be ran. * `js_browser` - V outputs JS source code ready for the browser. diff --git a/vlib/v/help/help_test.v b/vlib/v/help/help_test.v index 3720117af1a242..c84044c950a086 100644 --- a/vlib/v/help/help_test.v +++ b/vlib/v/help/help_test.v @@ -35,6 +35,12 @@ fn test_run_topic_mentions_conditional_cleanup() { assert res.output.contains('If the executable already existed before the command') } +fn test_build_topic_lists_fastc_backend() { + res := os.execute(vexe + ' help build') + assert res.exit_code == 0, res.output + assert res.output.contains('* `fastc`'), res.output +} + fn test_all_topics() { help_dir := os.join_path(@VEXEROOT, 'vlib', 'v', 'help') topic_paths := os.walk_ext(help_dir, '.txt') diff --git a/vlib/v/pref/pref.v b/vlib/v/pref/pref.v index b51b50017a6670..8300de91f7c491 100644 --- a/vlib/v/pref/pref.v +++ b/vlib/v/pref/pref.v @@ -94,6 +94,7 @@ pub mut: os OS // the OS to compile for backend Backend backend_set_by_flag bool // true when the compiler receives `-b`/`-backend` + is_fastc bool // true when the final `-b`/`-backend` option selects fastc build_mode BuildMode arch Arch output_mode OutputMode = .stdout @@ -457,6 +458,7 @@ fn parse_args_impl(known_external_commands []string, args []string, show_output mut no_skip_unused := false mut command, mut command_idx := '', 0 mut build_vsh_source := false + mut new_compiler_set_by_flag := false for i := 0; i < args.len; i++ { arg := args[i] if pass_external_command_args && command_idx < i && command in known_external_commands { @@ -563,6 +565,7 @@ fn parse_args_impl(known_external_commands []string, args []string, show_output } '-new-compiler' { res.new_compiler = true + new_compiler_set_by_flag = true } '-checker-fixture', '-macos-v3-compat-c99' { // Passed through to the embedded V3 diagnostic fixture runner. @@ -1129,9 +1132,10 @@ fn parse_args_impl(known_external_commands []string, args []string, show_output } '-b', '-backend' { sbackend := cmdline.option(args[i..], arg, 'c') + res.is_fastc = sbackend == 'fastc' res.build_options << '${arg} ${sbackend}' b := backend_from_string(sbackend) or { - eprintln_exit('Unknown V backend: ${sbackend}\nValid -backend choices are: c, js, js_node, js_browser, js_freestanding, wasm') + eprintln_exit('Unknown V backend: ${sbackend}\nValid -backend choices are: c, fastc, js, js_node, js_browser, js_freestanding, wasm') } if b == .wasm { res.compile_defines << 'wasm' @@ -1386,6 +1390,11 @@ fn parse_args_impl(known_external_commands []string, args []string, show_output } res.build_options = m.keys() // eprintln('>> res.build_options: ${res.build_options}') + // FastC belongs to the embedded V3 driver, but both `fastc` and `c` use + // Backend.c while cmd/v parses the command line. Only the final backend + // option should select V3 implicitly; an explicit -new-compiler remains an + // independent request. + res.new_compiler = new_compiler_set_by_flag || res.is_fastc res.fill_with_defaults() if res.generate_c_project != '' { // The generated C project should not depend on cached V module objects. @@ -1439,7 +1448,7 @@ pub fn backend_from_string(s string) !Backend { // TODO: unify the "different js backend" options into a single `-b js` // + a separate option, to choose the wanted JS output. return match s { - 'c' { .c } + 'c', 'fastc' { .c } 'eval', 'interpret' { eprintln_exit('The eval backend has been removed.') } 'js', 'js_node' { .js_node } 'js_browser' { .js_browser } diff --git a/vlib/v/pref/pref_test.v b/vlib/v/pref/pref_test.v index bce52c06d2d13f..56454ea055951d 100644 --- a/vlib/v/pref/pref_test.v +++ b/vlib/v/pref/pref_test.v @@ -553,6 +553,54 @@ fn test_new_compiler_flag_is_accepted() { assert '-new-compiler' !in prefs.build_options } +fn test_fastc_backend_selects_v3_driver() { + target := os.join_path(vroot, 'examples', 'hello_world.v') + prefs, command := pref.parse_args_and_show_errors([], ['-b', 'fastc', target], false) + assert command == target + assert prefs.backend == .c + assert prefs.backend_set_by_flag + assert prefs.is_fastc + assert prefs.new_compiler + assert prefs.build_options.contains('-b fastc') +} + +fn test_later_backend_overrides_fastc_v3_selection() { + target := os.join_path(vroot, 'examples', 'hello_world.v') + prefs, command := pref.parse_args_and_show_errors([], ['-b', 'fastc', '-b', 'c', target], false) + assert command == target + assert prefs.backend == .c + assert !prefs.is_fastc + assert !prefs.new_compiler +} + +fn test_final_fastc_backend_selects_v3_driver() { + target := os.join_path(vroot, 'examples', 'hello_world.v') + prefs, command := pref.parse_args_and_show_errors([], ['-b', 'c', '-b', 'fastc', target], false) + assert command == target + assert prefs.backend == .c + assert prefs.is_fastc + assert prefs.new_compiler +} + +fn test_explicit_new_compiler_survives_backend_override() { + target := os.join_path(vroot, 'examples', 'hello_world.v') + prefs, command := pref.parse_args_and_show_errors([], ['-new-compiler', '-b', 'fastc', '-b', + 'c', target], false) + assert command == target + assert !prefs.is_fastc + assert prefs.new_compiler +} + +fn test_repeated_backend_flags_preserve_final_fastc_selection() { + target := os.join_path(vroot, 'examples', 'hello_world.v') + prefs, command := pref.parse_args_and_show_errors([], ['-b', 'fastc', '-b', 'c', '-b', 'fastc', + target], false) + assert command == target + assert prefs.backend == .c + assert prefs.is_fastc + assert prefs.new_compiler +} + fn test_v3_checker_fixture_flag_is_accepted() { target := os.join_path(vroot, 'examples', 'hello_world.v') for flag in ['-checker-fixture', '-macos-v3-compat-c99'] { diff --git a/vlib/v3/README.md b/vlib/v3/README.md index d9ad1f66fbf26a..951be31ee7b812 100644 --- a/vlib/v3/README.md +++ b/vlib/v3/README.md @@ -3,11 +3,11 @@ Clean rewrite of the V compiler. Reuses v2's scanner, uses a flat AST parser with Pratt parsing, a structured type system with sum-type variants, lexical scoping, a transformer for AST simplification, a shared type-checking phase, a -markused pass for dead-code elimination, recursive import resolution, and three -backends: a direct flat-AST-to-C backend, a native ARM64 backend via SSA IR with -a built-in linker, and a direct flat-AST-to-WebAssembly backend. With `-prod`, -the ARM64 backend runs SSA optimization, MIR lowering, and instruction -selection. +markused pass for dead-code elimination, recursive import resolution, and +backends: a direct flat-AST-to-C backend, a scanner-to-C fast path, a native +ARM64 backend via SSA IR with a built-in linker, and a direct +flat-AST-to-WebAssembly backend. With `-prod`, the ARM64 backend runs SSA +optimization, MIR lowering, and instruction selection. Imports all `vlib/builtin/` V source files, both pure V (`.v`) and C-interop (`.c.v`), for struct, enum, type alias, interface, C function declarations, and @@ -88,6 +88,35 @@ Stage rows recorded at pipeline boundaries report sampled peak RSS and the proce breakdowns reconstructed after a stage omit the sampled peak. On macOS each row also prints physical footprint immediately after RSS. +## Fast C backend + +`-b fastc` selects the embedded V3 driver and its AST-free single-file parser for the shortest +edit-run cycle. FastC scans the source once and emits GNU C while consuming tokens. It never invokes +the flat parser, semantic checker, transformer, mark-used pass, or conventional C generator. +Bundled TinyCC validates the emitted translation unit before any C file or executable is published. +Unsupported V syntax and TinyCC errors are reported directly; FastC never retries through an +AST-based backend. + +FastC currently emits primitive functions and parameters, inferred local declarations, ordinary +expressions, `if`/`else`, and condition, C-style, infinite, and integer-range `for` loops. GNU +`typeof` carries `:=` declarations into C without V type inference. Integer-range bounds are +evaluated once, from left to right. The parser also rejects mutation of immutable or unknown local +names instead of relying on C's weaker assignment rules. + +Syntax whose V semantics require type or runtime information is rejected until FastC can lower it +directly. This includes float printing, C-string and embedded-NUL string literals, runes, +assertions, `sizeof`, comparison/logical, shift, division, modulo, indexing, parallel assignment, +mixed-precedence expressions, narrow integer signatures, oversized decimal literals, and +high-bit hexadecimal or binary literals. Rejecting these constructs avoids silently applying +incompatible C formatting, inference, wrapping, shift, bounds, and zero-divisor behavior. + +FastC requires exactly one `.v` input. Executables are host-target only; `-o file.c` also permits an +explicit cross target and publishes C after TinyCC validation. Production, test, shared/live, +ownership/autofree, self-host, object-file, profiling/coverage, strict C, custom compiler, +custom-builtin, `no_main`, translated, and REPL modes are currently rejected. Explicit FastC +compiler builds still route to V3 so the FastC parser reports the unsupported input rather than +silently selecting V1. + Generated C represents `thread` values with a typed wrapper around `pthread_t`. `spawn` and detached standard-library workers use the target's default thread stack (8 MiB on 64-bit targets and 2 MiB on 32-bit targets); `-thread-stack-size ` overrides it. Thread allocation, @@ -169,6 +198,8 @@ the plan and run the complete diagnostic and generation pipeline normally. ## Architecture ``` +source -> scanner -> fastc parser/C emitter -> TinyCC + source + vlib/builtin -> scanner -> flat parser -> flat AST -> imports -> check -> transform -> annotate types -> markused -> gen C -> cc \-> SSA build -> ARM64 gen -> link diff --git a/vlib/v3/driver/driver.v b/vlib/v3/driver/driver.v index 6dfdc471b10909..23ef2b42771cd1 100644 --- a/vlib/v3/driver/driver.v +++ b/vlib/v3/driver/driver.v @@ -13,6 +13,7 @@ import v3.flat import v3.fixturetest import v3.gen.c as cgen import v3.gen.c.naming +import v3.gen.fastc import v3.markused import v3.modulecache import v3.parser @@ -2194,7 +2195,7 @@ fn v3_crun_build_identity(state &V3ModuleCacheState, prefs &pref.Preferences, us fn cli_usage() string { return 'usage: v3 [run|test] [options]\n' + ' -o output binary or C file\n' + - ' -b backend\n' + + ' -b backend\n' + ' -os -arch target platform\n' + ' -cc C compiler executable\n' + ' -thread-stack-size spawned-thread stack size\n' + @@ -6393,6 +6394,59 @@ fn add_v3_profile_used_fns(mut used_fns map[string]bool) { } } +struct V3FastCCompileResult { + success bool + command string + output string +} + +fn compile_v3_fastc_source(source string, bin_file string, prefs &pref.Preferences, environment_c_flags []string, user_c_flags []string, environment_ld_flags []string, is_debug bool) V3FastCCompileResult { + tcc_dir := os.join_path(prefs.vroot, 'thirdparty', 'tcc') + tcc_path := os.join_path_single(tcc_dir, 'tcc.exe') + if !os.is_executable(tcc_path) { + return V3FastCCompileResult{} + } + build_dir := os.join_path_single(os.dir(os.real_path(bin_file)), + '.${os.file_name(bin_file)}.fastc.${tempname.unique_token()}') + os.mkdir_all(build_dir) or { return V3FastCCompileResult{} } + defer { + cleanup_c_build_dir(build_dir) + } + source_file := os.join_path_single(build_dir, 'src.c') + staged_binary := os.join_path_single(build_dir, 'out') + os.write_file(source_file, source) or { return V3FastCCompileResult{} } + tcc_lib_dir := os.join_path_single(tcc_dir, 'lib') + mut cc_args := environment_c_flags.clone() + cc_args << ['-std=gnu11', '-I${os.join_path_single(tcc_lib_dir, 'include')}', '-L${tcc_lib_dir}', + '-w'] + if is_debug { + cc_args << '-g' + } + cc_args << ['-o', 'out', 'src.c'] + cc_args << user_c_flags + cc_args << '-lm' + cc_args << environment_ld_flags + command := cmdexec.display(tcc_path, cc_args) + result := cmdexec.run_in(tcc_path, cc_args, build_dir) + if result.exit_code != 0 || !os.is_file(staged_binary) { + return V3FastCCompileResult{ + command: command + output: result.output + } + } + os.mv(staged_binary, bin_file) or { + return V3FastCCompileResult{ + command: command + output: err.msg() + } + } + return V3FastCCompileResult{ + success: true + command: command + output: result.output + } +} + // run executes the V3 compiler driver with `args`. @[markused] pub fn run(args []string) { @@ -6495,6 +6549,7 @@ pub fn run(args []string) { mut is_debug := false mut is_c_debug := false mut c99 := false + mut c99_explicit := false mut thread_stack_size := 0 mut thread_stack_size_set := false mut all_backends := false @@ -6612,8 +6667,11 @@ pub fn run(args []string) { i++ } else if args[i] in ['-c99', '--c99', macos_v3_compat_c99_flag] { c99 = true - if args[i] != macos_v3_compat_c99_flag && 'c99' !in user_defines { - user_defines << 'c99' + if args[i] != macos_v3_compat_c99_flag { + c99_explicit = true + if 'c99' !in user_defines { + user_defines << 'c99' + } } i++ } else if args[i] in ['-strict', '-cstrict'] { @@ -6918,13 +6976,13 @@ pub fn run(args []string) { // `-no-bounds-checking`, matching the established parser contract. user_defines = user_defines.filter(it.all_before('=').trim_space() != 'no_bounds_checking') } - if is_prof && backend != 'c' { + if is_prof && backend !in ['c', 'fastc'] { eprintln('option `-profile` is only supported by the C backend') exit(1) } should_run = should_run && !skip_running - if is_o && (backend != 'c' || !explicit_output || (!output_file.ends_with('.c') - && !output_file.ends_with('.o'))) { + if is_o && (backend !in ['c', 'fastc'] || !explicit_output + || (!output_file.ends_with('.c') && !output_file.ends_with('.o'))) { eprintln('option `-is_o` requires the C backend and an explicit `.c` or `.o` output file') exit(1) } @@ -7040,17 +7098,17 @@ pub fn run(args []string) { eprintln('unsupported garbage collector define `${define_name}`; v3 programs must not use a garbage collector') exit(1) } - if define_name == 'ownership' && !ownership_checker_compiled() { + if define_name == 'ownership' && backend != 'fastc' && !ownership_checker_compiled() { eprintln('ownership support is not compiled into this v3 executable') exit(1) } } - if ownership_mode && !ownership_checker_compiled() { + if ownership_mode && backend != 'fastc' && !ownership_checker_compiled() { eprintln('ownership support is not compiled into this v3 executable') exit(1) } - if backend !in ['c', 'arm64', 'wasm', 'eval'] { - eprintln('unknown backend `${backend}`; expected c, arm64, wasm, or eval') + if backend !in ['c', 'fastc', 'arm64', 'wasm', 'eval'] { + eprintln('unknown backend `${backend}`; expected c, fastc, arm64, wasm, or eval') exit(1) } if backend == 'arm64' && target_os != 'macos' && 'no_gettid' !in user_defines { @@ -7140,13 +7198,13 @@ pub fn run(args []string) { } else if backend == 'wasm' { // Honor the exact -o path; the wasm backend writes output_file directly. bin_file = output_file.all_before_last('.wasm') - } else if backend == 'c' && output_file == '-' { + } else if backend in ['c', 'fastc'] && output_file == '-' { c_only = true c_to_stdout = true bin_file = '' output_file = os.join_path_single(os.vtmp_dir(), 'v3_stdout_${os.getpid()}_${tempname.unique_token()}.c') - } else if backend == 'c' && output_file.ends_with('.c') { + } else if backend in ['c', 'fastc'] && output_file.ends_with('.c') { c_only = true bin_file = output_file.all_before_last('.c') } else { @@ -7156,7 +7214,7 @@ pub fn run(args []string) { } output_file = bin_file + '.c' } - if backend == 'c' { + if backend in ['c', 'fastc'] { target_bin_file := c_executable_bin_file_for_target(bin_file, target.os, is_shared, is_o, c_only) if target_bin_file != bin_file { @@ -7290,6 +7348,165 @@ pub fn run(args []string) { eprintln('v.pref.lookup_path: ${os.join_path(prefs.vroot, 'vlib')}') } prefs.supports_inline_asm = is_checker_fixture + if backend == 'fastc' { + // FastC is a standalone parser that emits C while consuming scanner tokens. + // Never let an unsupported FastC input continue into the AST frontend below. + clear_macos_v3_compiler_error_fallback(macos_v3_fallback_file) + if !input_file.ends_with('.v') || !os.is_file(input_file) || file_list.len > 0 { + eprintln('fastc requires exactly one `.v` input file') + exit(1) + } + fastc_host := pref.host_target() + if !c_only && (target.os != fastc_host.os || target.arch != fastc_host.arch) { + eprintln('fastc can only build executables for the host target; use `-o file.c` for cross-target C output') + exit(1) + } + mut unsupported_modes := []string{} + if is_test_command || is_checker_fixture { + unsupported_modes << 'test/checker mode' + } + if building_v || is_selfhost { + unsupported_modes << 'compiler self-hosting' + } + if is_prod { + unsupported_modes << '`-prod`' + } + if is_shared || is_livemain || is_liveshared { + unsupported_modes << 'shared/live builds' + } + if is_o { + unsupported_modes << 'object-file output' + } + if is_prof || coverage_dir.len > 0 { + unsupported_modes << 'profiling/coverage' + } + if ownership_mode || 'ownership' in prefs.user_defines { + unsupported_modes << 'ownership/autofree' + } + if only_check_syntax || check_only { + unsupported_modes << 'syntax/check-only mode' + } + if print_fn_names.len > 0 || print_v_files || print_watched_files || dump_c_flags.len > 0 + || generate_c_project.len > 0 { + unsupported_modes << 'compiler inspection output' + } + if c99_explicit || is_strict || check_overflow { + unsupported_modes << 'strict/checked C modes' + } + if c_compiler_explicit { + unsupported_modes << 'custom C compilers' + } + if no_builtin || no_preludes { + unsupported_modes << 'custom builtin/prelude modes' + } + if 'no_main' in prefs.user_defines { + unsupported_modes << '`-d no_main`' + } + if translated_mode || is_repl { + unsupported_modes << 'translated/REPL mode' + } + if unsupported_modes.len > 0 { + eprintln('fastc parser does not support ${unsupported_modes.join(', ')}') + exit(1) + } + source := os.read_file(input_file) or { + eprintln('fastc could not read `${input_file}`: ${err.msg()}') + exit(1) + '' + } + fastc_source := fastc.generate(source, input_file, prefs) or { + eprintln(err.msg()) + exit(1) + '' + } + b.step('fastc parse+gen') + // Validate generated C before publishing it. C-only builds use a throwaway + // executable; normal builds keep the binary produced by bundled TinyCC. + fastc_bin_file := if c_only { + os.join_path_single(os.vtmp_dir(), + 'v3_fastc_validate_${os.getpid()}_${tempname.unique_token()}') + } else { + bin_file + } + fastc_result := compile_v3_fastc_source(fastc_source, fastc_bin_file, prefs, + environment_c_flags, user_c_flags, environment_ld_flags, is_debug) + if (!silent || show_cc) && fastc_result.command.len > 0 { + if c_to_stdout { + eprintln(' > ${fastc_result.command}') + } else { + println(' > ${fastc_result.command}') + } + } + if show_c_output && fastc_result.output.len > 0 { + header := '======== Output of TinyCC fastc ========' + if c_to_stdout { + eprintln(header) + eprintln(fastc_result.output.trim_space()) + eprintln('='.repeat(header.len)) + } else { + println(header) + println(fastc_result.output.trim_space()) + println('='.repeat(header.len)) + } + } + if c_only { + os.rm(fastc_bin_file) or {} + } + if !fastc_result.success { + if fastc_result.command.len == 0 { + eprintln('fastc requires the bundled TinyCC executable') + } else if !show_c_output && fastc_result.output.len > 0 { + eprintln(fastc_result.output.trim_space()) + } + exit(1) + } + b.step('tcc') + b.metric('generated C size', fastc_source.len, 'bytes') + if c_only { + if c_to_stdout { + print(fastc_source) + } else { + staged_output := '${output_file}.stage.${tempname.unique_token()}' + os.write_file(staged_output, fastc_source) or { + eprintln('error writing fastc output ${output_file}: ${err.msg()}') + exit(1) + } + os.mv(staged_output, output_file) or { + os.rm(staged_output) or {} + eprintln('error finalizing fastc output ${output_file}: ${err.msg()}') + exit(1) + } + } + b.print_report() + clear_macos_v3_compiler_error_fallback(macos_v3_fallback_file) + return + } + if backend_explicit { + os.write_file(bin_file + '.c', fastc_source) or { + eprintln('failed to retain generated fastc output ${bin_file}.c: ${err.msg()}') + exit(1) + } + } + if keep_c { + keep_c_file := keep_c_output_file(bin_file) + os.write_file(keep_c_file, fastc_source) or { + eprintln('failed to retain generated fastc output ${keep_c_file}: ${err.msg()}') + exit(1) + } + } + if should_run { + run_result := run_binary(bin_file, run_args) + if remove_binary_after_run { + os.rm(bin_file) or {} + } + if run_result != 0 { + exit(run_result) + } + b.step('run') + } + b.print_report() + return + } minimal_literal_output := !is_prof && input_uses_minimal_literal_output_builtin(input_file, prefs, is_test_command, is_checker_fixture) host_target := pref.host_target() @@ -8966,6 +9183,8 @@ pub fn run(args []string) { if !cgen_cache_hit && scope_prealloc_cgen && test_files.len == 0 { cgen_parse_cache_enabled := pre_tc.type_cache_parse_enabled() cgen_scope := prealloc_scope_begin_for_v3() + mut scoped_generated_c_flags := []string{} + generated_path := if cache_state.manager.enabled { cache_plan_file } else { cc_src } mut g := cgen.FlatGen.new() g.set_initial_c_flags(user_c_flags) g.set_c99_mode(prefs.c99) @@ -8997,7 +9216,6 @@ pub fn run(args []string) { g.set_incremental_fn_names(incremental_changed_names) g.set_cached_support_declarations(incremental_known_declarations) g.set_scope_parallel_workers(!generic_cache_hit) - generated_path := if cache_state.manager.enabled { cache_plan_file } else { cc_src } g.gen_to_file_with_used_test_options(generated_path, a, cgen_used_fns, &pre_tc, cache_no_parallel_cgen, test_files) or { eprintln('error writing ${generated_path}: ${err}') @@ -9005,11 +9223,13 @@ pub fn run(args []string) { exit(1) } cgen_was_parallel = g.was_parallel() - scoped_c_flags := g.c_flags() + if !incremental_cache_hit { + scoped_generated_c_flags = g.c_flags() + } g.free_parallel_worker_scopes() prealloc_scope_leave_for_v3(cgen_scope) if !incremental_cache_hit { - generated_c_flags = clone_string_list(scoped_c_flags) + generated_c_flags = clone_string_list(scoped_generated_c_flags) } // Cgen's synchronous type queries memoize through the shared checker. // Reattach empty parent-owned interners and caches before releasing its @@ -9018,6 +9238,7 @@ pub fn run(args []string) { pre_tc.set_fresh_type_cache(cgen_parse_cache_enabled) prealloc_scope_free_for_v3(cgen_scope) } else if !cgen_cache_hit { + generated_path := if cache_state.manager.enabled { cache_plan_file } else { cc_src } mut g := cgen.FlatGen.new() g.set_initial_c_flags(user_c_flags) g.set_c99_mode(prefs.c99) @@ -9048,7 +9269,6 @@ pub fn run(args []string) { g.set_cache_program_files(user_files) g.set_incremental_fn_names(incremental_changed_names) g.set_cached_support_declarations(incremental_known_declarations) - generated_path := if cache_state.manager.enabled { cache_plan_file } else { cc_src } g.gen_to_file_with_used_test_options(generated_path, a, cgen_used_fns, &pre_tc, cache_no_parallel_cgen, test_files) or { eprintln('error writing ${generated_path}: ${err}') diff --git a/vlib/v3/gen/fastc/fastc.v b/vlib/v3/gen/fastc/fastc.v new file mode 100644 index 00000000000000..8014296d31bc11 --- /dev/null +++ b/vlib/v3/gen/fastc/fastc.v @@ -0,0 +1,1473 @@ +module fastc + +import strings +import v3.pref +import v3.scanner +import v3.token + +// FastC parses scanner tokens and emits C immediately. It deliberately has no +// AST, semantic-checker, transformer, mark-used, or conventional cgen path. + +const c_preamble = r'#include +#include +#include +#include + +typedef int8_t i8; +typedef int16_t i16; +typedef int32_t i32; +typedef int64_t i64; +typedef uint8_t u8; +typedef uint16_t u16; +typedef uint32_t u32; +typedef uint64_t u64; +typedef intptr_t isize; +typedef uintptr_t usize; +typedef unsigned char byte; +typedef int32_t rune; +typedef float f32; +typedef double f64; +typedef const char *string; +typedef void *voidptr; +typedef unsigned char *byteptr; +typedef char *charptr; + +static void v_fastc_print_string(const char *value) { fputs(value, stdout); } +static void v_fastc_print_bool(bool value) { fputs(value ? "true" : "false", stdout); } +static void v_fastc_print_char(char value) { fputc(value, stdout); } +static void v_fastc_print_signed(long long value) { printf("%lld", value); } +static void v_fastc_print_unsigned(unsigned long long value) { printf("%llu", value); } +static void v_fastc_println_string(const char *value) { puts(value); } +static void v_fastc_println_bool(bool value) { puts(value ? "true" : "false"); } +static void v_fastc_println_char(char value) { fputc(value, stdout); fputc(10, stdout); } +static void v_fastc_println_signed(long long value) { printf("%lld\n", value); } +static void v_fastc_println_unsigned(unsigned long long value) { printf("%llu\n", value); } + +/* Float formatting belongs to the V strconv routines. Leaving float and double + * unmatched makes TinyCC reject unsupported printing instead of silently + * applying printf %g semantics. */ +#define V_FASTC_PRINT_SELECT(value, string_fn, bool_fn, char_fn, signed_fn, unsigned_fn) _Generic((value), char *: string_fn, const char *: string_fn, bool: bool_fn, char: char_fn, signed char: signed_fn, short: signed_fn, int: signed_fn, long: signed_fn, long long: signed_fn, unsigned char: unsigned_fn, unsigned short: unsigned_fn, unsigned int: unsigned_fn, unsigned long: unsigned_fn, unsigned long long: unsigned_fn)(value) +#define print(value) V_FASTC_PRINT_SELECT(value, v_fastc_print_string, v_fastc_print_bool, v_fastc_print_char, v_fastc_print_signed, v_fastc_print_unsigned) +#define println(value) V_FASTC_PRINT_SELECT(value, v_fastc_println_string, v_fastc_println_bool, v_fastc_println_char, v_fastc_println_signed, v_fastc_println_unsigned) + +' + +struct FastcFunctionSignature { + parameter_types []string + return_type string +} + +struct FastcLocal { + is_mut bool + typ string +} + +struct FastcExpressionToken { + tok token.Token + lit string +} + +struct Parser { + path string +mut: + s scanner.Scanner + tok token.Token + lit string + out strings.Builder + protos strings.Builder + indent int + in_main bool + has_main bool + unsafe_depth int + temp_id int + locals map[string]FastcLocal + functions map[string]FastcFunctionSignature + return_type string + last_expression_type string + last_expression []FastcExpressionToken +} + +// generate scans V source and emits C as each declaration and statement is consumed. It does +// not construct an AST or invoke semantic type checking. Unsupported syntax is returned as an +// error; FastC never retries through an AST-based backend. +pub fn generate(source string, path string, prefs &pref.Preferences) !string { + functions := collect_function_signatures(source, path, prefs)! + mut file_set := token.FileSet.new() + mut file := file_set.add_file(path, source.len) + file.index_lines(source) + mut gen := Parser{ + path: path + s: scanner.new_scanner(prefs, .normal) + out: strings.new_builder(source.len) + protos: strings.new_builder(256) + functions: functions + } + gen.s.init(file, source) + generated := gen.run()! + if gen.s.diagnostics.len > 0 { + diagnostic := gen.s.diagnostics[0] + return error('fastc scanner error at byte ${diagnostic.offset} in ${path}: ${diagnostic.message}') + } + return generated +} + +fn collect_function_signatures(source string, path string, prefs &pref.Preferences) !map[string]FastcFunctionSignature { + mut file_set := token.FileSet.new() + mut file := file_set.add_file(path, source.len) + file.index_lines(source) + mut scan := scanner.new_scanner(prefs, .normal) + scan.init(file, source) + mut functions := map[string]FastcFunctionSignature{} + mut brace_depth := 0 + mut tok := scan.scan() + for tok != .eof { + if tok == .key_fn && brace_depth == 0 { + tok = scan.scan() + if tok != .name { + return error('fastc parser does not support function declaration in ${path}') + } + name := scan.lit + if name in functions { + return error('fastc parser does not support duplicate function `${name}` in ${path}') + } + tok = scan.scan() + if tok != .lpar { + return error('fastc parser does not support function `${name}` declaration in ${path}') + } + tok = scan.scan() + mut parameter_types := []string{} + for tok != .rpar { + if tok in [.key_mut, .key_shared] { + return error('fastc parser does not support mutable or shared parameters in ${path}') + } + if tok != .name { + return error('fastc parser does not support function parameters in ${path}') + } + tok = scan.scan() + if tok == .comma { + return error('fastc parser does not support grouped parameter names in ${path}') + } + parameter_type, next_token := fastc_scan_type(mut scan, tok, path)! + parameter_types << parameter_type + tok = next_token + if tok == .comma { + tok = scan.scan() + continue + } + if tok != .rpar { + return error('fastc parser does not support function parameter separator in ${path}') + } + } + tok = scan.scan() + mut return_type := 'void' + if tok != .lcbr { + return_type, tok = fastc_scan_type(mut scan, tok, path)! + } + if tok != .lcbr { + return error('fastc parser does not support function `${name}` body in ${path}') + } + functions[name] = FastcFunctionSignature{ + parameter_types: parameter_types + return_type: return_type + } + continue + } + if tok == .lcbr { + brace_depth++ + } else if tok == .rcbr && brace_depth > 0 { + brace_depth-- + } + tok = scan.scan() + } + return functions +} + +fn fastc_scan_type(mut scan scanner.Scanner, first token.Token, path string) !(string, token.Token) { + mut tok := first + mut pointers := 0 + for tok == .amp || tok == .mul { + pointers++ + tok = scan.scan() + } + if tok != .name { + return error('fastc parser does not support type `${tok.str()}` in ${path}') + } + raw_type := scan.lit + base := fastc_primitive_c_type(raw_type) or { + return error('fastc parser does not support undeclared type `${raw_type}` in ${path}') + } + tok = scan.scan() + if tok in [.dot, .lsbr, .question, .not] { + return error('fastc parser does not support compound type `${raw_type}` in ${path}') + } + return base + '*'.repeat(pointers), tok +} + +fn (mut g Parser) run() !string { + g.next() + for g.tok != .eof { + g.skip_semicolons() + if g.tok == .eof { + break + } + if g.tok == .key_module { + g.parse_module()! + continue + } + if g.tok == .key_pub || g.tok == .key_static { + g.next() + } + if g.tok == .key_fn { + g.parse_function()! + continue + } + if g.tok == .key_import { + return g.unsupported('top-level `${g.token_source()}`') + } + if g.has_main { + return g.unsupported('top-level `${g.token_source()}` after `main`') + } + g.parse_script()! + break + } + mut result := strings.new_builder(c_preamble.len + g.protos.len + g.out.len + 2) + result.write_string(c_preamble) + result.write_string(g.protos.str()) + result.writeln('') + result.write_string(g.out.str()) + return result.str() +} + +fn (mut g Parser) next() { + g.tok = g.s.scan() + g.lit = g.s.lit +} + +fn (mut g Parser) temporary_name(kind string) string { + name := '__v_fastc_${kind}_${g.temp_id}' + g.temp_id++ + return name +} + +fn (mut g Parser) skip_semicolons() { + for g.tok == .semicolon { + g.next() + } +} + +fn (g &Parser) unsupported(feature string) IError { + return error('fastc parser does not support ${feature} in ${g.path}') +} + +fn (mut g Parser) expect(expected token.Token) ! { + if g.tok != expected { + return g.unsupported('`${expected.str()}` after `${g.token_source()}`') + } + g.next() +} + +fn (mut g Parser) parse_module() ! { + g.next() + if g.tok != .name { + return g.unsupported('module declaration') + } + // A single-file unit has no module namespace to resolve. `main` is accepted + // and discarded; every other module is reported as unsupported. + if g.lit != 'main' { + return g.unsupported('module `${g.lit}`') + } + g.next() + g.skip_semicolons() +} + +fn (mut g Parser) parse_function() ! { + g.locals = map[string]FastcLocal{} + g.next() + if g.tok == .lpar { + return g.unsupported('methods') + } + if g.tok != .name { + return g.unsupported('function declaration') + } + name := g.lit + g.next() + if g.tok == .lsbr { + return g.unsupported('generic functions') + } + g.expect(.lpar)! + params := g.parse_parameters()! + mut return_type := 'void' + if g.tok != .lcbr { + return_type = g.parse_type()! + } + if fastc_has_narrow_integer_type(return_type) + || params.any(fastc_parameter_has_narrow_integer_type) { + // C promotes narrow operands before arithmetic, while V retains the narrow + // result type. Reject them until the direct parser tracks the required type. + return g.unsupported('narrow integer function types') + } + if name == 'main' { + if params.len > 0 { + return g.unsupported('main function with parameters') + } + if return_type != 'void' { + return g.unsupported('main function returning `${return_type}`') + } + } + g.expect(.lcbr)! + is_main := name == 'main' + if is_main { + g.has_main = true + } + c_return_type := if is_main { 'int' } else { return_type } + c_params := if params.len == 0 { 'void' } else { params.join(', ') } + g.protos.writeln('${c_return_type} ${name}(${c_params});') + g.write_line('${c_return_type} ${name}(${c_params}) {') + g.indent++ + if is_main { + g.write_line('setvbuf(stdout, NULL, _IONBF, 0);') + } + previous_in_main := g.in_main + previous_return_type := g.return_type + g.in_main = is_main + g.return_type = return_type + terminates := g.parse_block_body()! + g.in_main = previous_in_main + g.return_type = previous_return_type + if return_type != 'void' && !terminates { + return g.unsupported('non-void function `${name}` that can fall through') + } + if is_main { + g.write_line('return 0;') + } + g.indent-- + g.write_line('}') + g.out.writeln('') +} + +fn (mut g Parser) parse_script() ! { + g.locals = map[string]FastcLocal{} + g.has_main = true + g.protos.writeln('int main(void);') + g.write_line('int main(void) {') + g.indent++ + g.write_line('setvbuf(stdout, NULL, _IONBF, 0);') + g.in_main = true + g.skip_semicolons() + for g.tok != .eof { + if g.tok in [.key_module, .key_pub, .key_static, .key_fn] { + return g.unsupported('declaration after top-level statements') + } + _ = g.parse_statement()! + g.skip_semicolons() + } + g.write_line('return 0;') + g.indent-- + g.write_line('}') + g.out.writeln('') +} + +fn (mut g Parser) parse_parameters() ![]string { + mut params := []string{} + g.skip_semicolons() + for g.tok != .rpar { + if g.tok in [.key_mut, .key_shared] { + return g.unsupported('mutable or shared parameters') + } + if g.tok != .name { + return g.unsupported('function parameters') + } + name := g.lit + g.next() + if g.tok == .comma { + return g.unsupported('grouped parameter names') + } + type_name := g.parse_type()! + params << '${type_name} ${name}' + g.locals[name] = FastcLocal{ + typ: type_name + } + if g.tok == .comma { + g.next() + g.skip_semicolons() + continue + } + if g.tok != .rpar { + return g.unsupported('function parameter separator') + } + } + g.next() + return params +} + +fn (mut g Parser) parse_type() !string { + mut pointers := 0 + for g.tok == .amp || g.tok == .mul { + pointers++ + g.next() + } + if g.tok != .name { + return g.unsupported('type `${g.token_source()}`') + } + raw_type := g.lit + if raw_type == 'charptr' || (raw_type == 'char' && pointers > 0) { + return g.unsupported('character pointer types') + } + if raw_type == 'rune' { + return g.unsupported('rune types') + } + g.next() + if g.tok in [.dot, .lsbr, .question, .not] { + return g.unsupported('compound type `${raw_type}`') + } + base := fastc_primitive_c_type(raw_type) or { + return g.unsupported('undeclared type `${raw_type}`') + } + return base + '*'.repeat(pointers) +} + +fn fastc_primitive_c_type(raw_type string) ?string { + return match raw_type { + 'bool' { 'bool' } + 'byte' { 'byte' } + 'char' { 'char' } + 'f32' { 'f32' } + 'f64' { 'f64' } + 'i8' { 'i8' } + 'i16' { 'i16' } + 'i32' { 'i32' } + 'i64' { 'i64' } + 'int' { 'int' } + 'isize' { 'isize' } + 'rune' { 'rune' } + 'string' { 'string' } + 'u8' { 'u8' } + 'u16' { 'u16' } + 'u32' { 'u32' } + 'u64' { 'u64' } + 'uint' { 'unsigned int' } + 'usize' { 'usize' } + 'voidptr' { 'voidptr' } + 'byteptr' { 'byteptr' } + 'charptr' { 'charptr' } + else { none } + } +} + +fn fastc_has_narrow_integer_type(type_name string) bool { + return type_name.trim_right('*') in ['byte', 'char', 'i8', 'i16', 'u8', 'u16'] +} + +fn fastc_parameter_has_narrow_integer_type(parameter string) bool { + fields := parameter.fields() + return fields.len > 0 && fastc_has_narrow_integer_type(fields[0]) +} + +fn (mut g Parser) parse_block_body() !bool { + mut terminates := false + g.skip_semicolons() + for g.tok != .rcbr { + if g.tok == .eof { + return g.unsupported('unfinished block') + } + statement_terminates := g.parse_statement()! + if statement_terminates { + terminates = true + } + g.skip_semicolons() + } + g.next() + g.skip_semicolons() + return terminates +} + +fn (mut g Parser) parse_statement() !bool { + return match g.tok { + .key_if { + g.parse_if()! + } + .key_for { + g.parse_for()! + } + .key_return { + g.parse_return()! + } + .key_break { + g.next() + g.consume_statement_end() + g.write_line('break;') + false + } + .key_continue { + g.next() + g.consume_statement_end() + g.write_line('continue;') + false + } + .key_mut { + g.parse_mutable_declaration()! + false + } + .key_unsafe { + g.next() + g.expect(.lcbr)! + g.unsafe_depth++ + terminates := g.parse_block_body()! + g.unsafe_depth-- + terminates + } + else { + g.parse_simple_statement()! + false + } + } +} + +fn (mut g Parser) parse_if() !bool { + g.next() + condition := g.read_expression([token.Token.lcbr])! + if condition.len == 0 { + return g.unsupported('empty if condition') + } + g.require_boolean_condition('if')! + g.expect(.lcbr)! + g.write_line('if (${condition}) {') + g.indent++ + then_terminates := g.parse_block_body()! + g.indent-- + if g.tok != .key_else { + g.write_line('}') + return false + } + g.next() + if g.tok == .key_if { + g.write_line('} else {') + g.indent++ + else_terminates := g.parse_if()! + g.indent-- + g.write_line('}') + return then_terminates && else_terminates + } + g.expect(.lcbr)! + g.write_line('} else {') + g.indent++ + else_terminates := g.parse_block_body()! + g.indent-- + g.write_line('}') + return then_terminates && else_terminates +} + +fn (mut g Parser) parse_for() !bool { + g.next() + if g.tok == .lcbr { + g.next() + g.write_line('for (;;) {') + g.indent++ + _ = g.parse_block_body()! + g.indent-- + g.write_line('}') + return false + } + if g.tok == .name { + name := g.lit + g.next() + if g.tok == .key_in { + if name in g.locals { + return g.unsupported('redeclaration of `${name}`') + } + g.next() + start := g.read_expression([token.Token.dotdot])! + start_expression_type := g.last_expression_type + g.expect(.dotdot)! + end := g.read_expression([token.Token.lcbr])! + end_expression_type := g.last_expression_type + if !fastc_is_integer_expression_type(start_expression_type) + || !fastc_is_integer_expression_type(end_expression_type) { + return g.unsupported('range bounds of types `${start_expression_type}` and `${end_expression_type}` must both be integers') + } + g.expect(.lcbr)! + start_name := g.temporary_name('range_start') + end_name := g.temporary_name('range_end') + // V evaluates both range bounds exactly once, from left to right. + g.write_line('__typeof__((${start})) ${start_name} = (${start});') + g.write_line('__typeof__((${end})) ${end_name} = (${end});') + g.write_line('for (__typeof__((${start_name})) ${name} = (${start_name}); ${name} < (${end_name}); ${name}++) {') + g.locals[name] = FastcLocal{ + typ: fastc_normalize_inferred_type(start_expression_type) + } + g.indent++ + _ = g.parse_block_body()! + g.indent-- + g.locals.delete(name) + g.write_line('}') + return false + } + if g.tok == .decl_assign { + if name in g.locals { + return g.unsupported('redeclaration of `${name}`') + } + g.next() + initial := g.read_expression([token.Token.semicolon])! + initial_type := fastc_normalize_inferred_type(g.last_expression_type) + g.expect(.semicolon)! + g.locals[name] = FastcLocal{ + is_mut: true + typ: initial_type + } + condition := g.read_expression([token.Token.semicolon])! + g.require_boolean_condition('for')! + g.expect(.semicolon)! + update := g.read_expression([token.Token.lcbr])! + g.expect(.lcbr)! + g.write_line('for (__typeof__((${initial})) ${name} = (${initial}); ${condition}; ${update}) {') + g.indent++ + _ = g.parse_block_body()! + g.indent-- + g.locals.delete(name) + g.write_line('}') + return false + } + g.validate_expression_name(name, .unknown)! + condition := g.read_expression_with_prefix(name, [token.Token.lcbr])! + g.require_boolean_condition('for')! + g.expect(.lcbr)! + g.write_line('while (${condition}) {') + g.indent++ + _ = g.parse_block_body()! + g.indent-- + g.write_line('}') + return false + } + condition := g.read_expression([token.Token.lcbr])! + g.require_boolean_condition('for')! + g.expect(.lcbr)! + g.write_line('while (${condition}) {') + g.indent++ + _ = g.parse_block_body()! + g.indent-- + g.write_line('}') + return false +} + +fn (mut g Parser) parse_return() !bool { + g.next() + if g.tok == .semicolon || g.tok == .rcbr { + if !g.in_main && g.return_type != 'void' { + return g.unsupported('bare return in non-void function') + } + g.consume_statement_end() + g.write_line(if g.in_main { 'return 0;' } else { 'return;' }) + return true + } + if g.return_type == 'void' { + return g.unsupported('value return in void function') + } + expression := g.read_expression([token.Token.semicolon, token.Token.rcbr])! + actual_type := g.last_expression_type + if actual_type.len == 0 { + return g.unsupported('unverifiable return expression type') + } + if !fastc_call_types_are_compatible(actual_type, g.return_type) { + return g.unsupported('return expression of type `${actual_type}` in function returning `${g.return_type}`') + } + g.consume_statement_end() + g.write_line('return ${expression};') + return true +} + +fn (g &Parser) require_boolean_condition(kind string) ! { + if g.last_expression_type.len == 0 { + return g.unsupported('unverifiable ${kind} condition type') + } + if g.last_expression_type != 'bool' { + return g.unsupported('${kind} condition of type `${g.last_expression_type}` instead of `bool`') + } +} + +fn (mut g Parser) parse_mutable_declaration() ! { + g.next() + if g.tok != .name { + return g.unsupported('mutable declaration') + } + name := g.lit + g.next() + if g.tok != .decl_assign { + return g.unsupported('`mut` statement without `:=`') + } + g.parse_declaration_after_name(name, true)! +} + +fn (mut g Parser) parse_simple_statement() ! { + if g.tok == .key_assert { + return g.unsupported('assert statements') + } + if g.tok == .name { + name := g.lit + g.next() + if g.tok == .decl_assign { + g.parse_declaration_after_name(name, false)! + return + } + if (g.tok.is_assignment() || g.tok in [.inc, .dec]) + && (name !in g.locals || !g.locals[name].is_mut) { + return g.unsupported('mutation of immutable or unknown name `${name}`') + } + g.validate_expression_name(name, .unknown)! + if g.tok.is_assignment() { + if g.tok in [.left_shift_assign, .right_shift_assign, .right_shift_unsigned_assign] { + return g.unsupported('shift expressions') + } + if g.tok in [.div_assign, .mod_assign] { + return g.unsupported('division or modulo expressions') + } + operator := g.tok + g.next() + value := g.read_expression([token.Token.semicolon, token.Token.rcbr])! + if value.len == 0 { + return g.unsupported('empty assignment to `${name}`') + } + actual_type := g.last_expression_type + expected_type := g.locals[name].typ + if actual_type.len == 0 || expected_type.len == 0 { + return g.unsupported('unverifiable assignment type for `${name}`') + } + if !fastc_call_types_are_compatible(actual_type, expected_type) { + return g.unsupported('assignment of type `${actual_type}` to `${name}` of type `${expected_type}`') + } + if operator != .assign && (!fastc_is_numeric_expression_type(actual_type) + || !fastc_is_numeric_expression_type(expected_type)) { + return g.unsupported('arithmetic assignment `${operator.str()}` on non-numeric type `${expected_type}`') + } + g.consume_statement_end() + g.write_line('${name}${operator.str()}${value};') + return + } + expression := + g.read_expression_with_prefix(name, [token.Token.semicolon, token.Token.rcbr])! + if !g.last_expression_is_statement() { + return g.unsupported('value-only expression statement') + } + g.consume_statement_end() + g.write_line('${expression};') + return + } + expression := g.read_expression([token.Token.semicolon, token.Token.rcbr])! + if expression.len == 0 { + return g.unsupported('statement `${g.token_source()}`') + } + return g.unsupported('value-only expression statement') +} + +fn (g &Parser) last_expression_is_statement() bool { + tokens := g.last_expression + if tokens.len == 2 && tokens[0].tok == .name && tokens[1].tok in [.inc, .dec] { + return true + } + if tokens.len < 3 || tokens[0].tok != .name || tokens[1].tok != .lpar { + return false + } + call_close := fastc_matching_rpar(tokens, 1) or { return false } + if call_close != tokens.len - 1 { + return false + } + name := tokens[0].lit + return name in g.functions || name in ['print', 'println'] +} + +fn (mut g Parser) parse_declaration_after_name(name string, is_mut bool) ! { + if name in g.locals { + return g.unsupported('redeclaration of `${name}`') + } + g.next() + expression := g.read_expression([token.Token.semicolon, token.Token.rcbr])! + if expression.len == 0 { + return g.unsupported('empty declaration') + } + g.consume_statement_end() + // GNU typeof is unevaluated and is supported by bundled TinyCC. It lets the + // direct path preserve V's `:=` without running any inference or type checker. + if expression.starts_with('"') { + // C's typeof preserves a literal's array type instead of applying the usual + // pointer decay. The spelling alone is enough to lower this case. + g.write_line('string ${name} = (${expression});') + } else { + g.write_line('__typeof__((${expression})) ${name} = (${expression});') + } + g.locals[name] = FastcLocal{ + is_mut: is_mut + typ: fastc_normalize_inferred_type(g.last_expression_type) + } +} + +fn fastc_normalize_inferred_type(typ string) string { + return match typ { + 'integer literal' { 'int' } + 'float literal' { 'f64' } + else { typ } + } +} + +fn (mut g Parser) consume_statement_end() { + if g.tok == .semicolon { + g.next() + } +} + +fn (mut g Parser) read_expression(stops []token.Token) !string { + return g.read_expression_with_prefix('', stops) +} + +fn (mut g Parser) read_expression_with_prefix(prefix string, stops []token.Token) !string { + mut result := strings.new_builder(64) + mut expression_tokens := []FastcExpressionToken{} + if prefix.len > 0 { + result.write_string(prefix) + expression_tokens << FastcExpressionToken{ + tok: .name + lit: prefix + } + } + mut paren_depth := 0 + mut has_sum_arithmetic_operator := false + mut has_multiply_operator := false + mut has_and_operator := false + mut has_pipe_operator := false + mut has_xor_operator := false + mut previous_token := token.Token.unknown + for g.tok != .eof { + if paren_depth == 0 && g.tok in stops { + break + } + if paren_depth == 0 && g.tok == .comma { + // V's top-level commas form simultaneous multi-target assignments. + // Copying them to C would instead emit comma operators. + return g.unsupported('parallel assignments') + } + if g.tok in [.eq, .ne, .gt, .lt, .ge, .le, .and, .logical_or, .not] { + // C represents comparison and logical results as int. Without V type + // information, accepting them here would make generic printing and + // inferred locals observe 0/1 instead of false/true. + return g.unsupported('comparison or logical expressions') + } + if g.tok in [.left_shift, .right_shift, .right_shift_unsigned, .left_shift_assign, + .right_shift_assign, .right_shift_unsigned_assign] { + // V defines oversized shifts to produce zero. Raw C shifts are + // undefined and may mask the count to the operand width instead. + return g.unsupported('shift expressions') + } + if g.tok in [.div, .div_assign, .mod, .mod_assign] { + // Integer division and modulo require V's runtime zero checks. This + // scanner-only lane has no type information to add them selectively. + return g.unsupported('division or modulo expressions') + } + if g.tok == .key_sizeof { + // Direct C representations can differ from V layouts. Reject sizeof + // until the parser tracks enough V type information to lower it. + return g.unsupported('sizeof expressions') + } + if g.tok in [.lsbr, .rsbr] { + // Indexing requires V element types and bounds checks. C pointer/array + // indexing cannot preserve either in this scanner-only lane. + return g.unsupported('expression token `${g.token_source()}`') + } + if g.tok in [.lcbr, .rcbr, .str_dollar, .key_match, .key_or, .key_as, .key_is, .not_is, + .key_in, .not_in, .arrow, .power] { + return g.unsupported('expression token `${g.token_source()}`') + } + match g.tok { + .plus, .minus { + has_sum_arithmetic_operator = true + } + .mul { + has_multiply_operator = true + } + .amp { + has_and_operator = true + } + .pipe { + has_pipe_operator = true + } + .xor { + has_xor_operator = true + } + else {} + } + if (has_sum_arithmetic_operator && (has_and_operator || has_pipe_operator + || has_xor_operator)) || (has_multiply_operator && has_and_operator) + || (has_pipe_operator && has_xor_operator) { + // V groups + and - with | and ^, and * with &, while C splits those + // levels and also orders + and - above &. Reject ambiguous token streams. + return g.unsupported('mixed operator precedence') + } + expression_tokens << FastcExpressionToken{ + tok: g.tok + lit: g.lit + } + piece := g.expression_token(previous_token)! + if result.len > 0 && fastc_needs_space(result.last(), piece) { + result.write_u8(` `) + } + result.write_string(piece) + match g.tok { + .lpar { + paren_depth++ + } + .rpar { + if paren_depth == 0 { + break + } + paren_depth-- + } + else {} + } + previous_token = g.tok + g.next() + } + if paren_depth != 0 { + return g.unsupported('unbalanced expression') + } + g.validate_expression_calls(expression_tokens)! + g.last_expression_type = g.infer_expression_type(expression_tokens)! + g.last_expression = expression_tokens + return result.str().trim_space() +} + +fn (g &Parser) expression_token(previous token.Token) !string { + return match g.tok { + .name { g.expression_name(previous)! } + .number { fastc_c_number(g.lit)! } + .string { fastc_c_string(g.lit)! } + .char { g.unsupported('rune or C character literals') } + // stdbool's true/false macros have C type int. Cast them so _Generic + // dispatch preserves V's bool type when no operator requires promotion. + .key_true { '((bool)true)' } + .key_false { '((bool)false)' } + .key_nil { g.nil_expression()! } + .key_likely, .key_unlikely { '' } + .semicolon { ';' } + else { g.tok.str() } + } +} + +fn (g &Parser) nil_expression() !string { + if g.unsafe_depth == 0 { + return g.unsupported('`nil` outside an `unsafe` block') + } + return 'NULL' +} + +fn (g &Parser) expression_name(previous token.Token) !string { + g.validate_expression_name(g.lit, previous)! + return g.lit +} + +fn (g &Parser) validate_expression_name(name string, previous token.Token) ! { + if fastc_has_narrow_integer_type(name) { + // C promotes narrow operands before arithmetic. Reject narrow casts in + // expressions until FastC can explicitly restore V's wrapping result type. + return g.unsupported('narrow integer cast expressions') + } + if name == 'charptr' { + return g.unsupported('charptr expressions') + } + if name == 'rune' { + return g.unsupported('rune expressions') + } + if previous == .dot || name in g.locals || name in g.functions + || name in ['print', 'println', 'bool', 'byte', 'char', 'f32', 'f64', 'i8', 'i16', 'i32', 'i64', 'int', 'isize', 'string', 'u8', 'u16', 'u32', 'u64', 'uint', 'usize', 'voidptr', 'byteptr'] { + return + } + return g.unsupported('unresolved name `${name}`') +} + +fn (g &Parser) validate_expression_calls(tokens []FastcExpressionToken) ! { + mut i := 0 + for i + 1 < tokens.len { + if tokens[i].tok != .name || tokens[i + 1].tok != .lpar { + i++ + continue + } + call_end := fastc_matching_rpar(tokens, i + 1) or { + return g.unsupported('unbalanced function call `${tokens[i].lit}`') + } + call_args := fastc_call_arguments(tokens, i + 1, call_end) or { + return g.unsupported('function call `${tokens[i].lit}` arguments') + } + for argument in call_args { + g.validate_expression_calls(argument)! + } + name := tokens[i].lit + if signature := g.functions[name] { + if call_args.len != signature.parameter_types.len { + return g.unsupported('function `${name}` call with ${call_args.len} arguments instead of ${signature.parameter_types.len}') + } + for argument_index, argument in call_args { + actual_type := g.infer_expression_type(argument)! + expected_type := signature.parameter_types[argument_index] + if actual_type.len == 0 { + return g.unsupported('unverifiable argument ${argument_index + 1} to function `${name}`') + } + if !fastc_call_types_are_compatible(actual_type, expected_type) { + return g.unsupported('argument ${argument_index + 1} of type `${actual_type}` to function `${name}` expecting `${expected_type}`') + } + } + } else if name in ['print', 'println'] { + if call_args.len != 1 { + return g.unsupported('function `${name}` call with ${call_args.len} arguments') + } + _ = g.infer_expression_type(call_args[0])! + } else if _ := fastc_primitive_c_type(name) { + if call_args.len != 1 { + return g.unsupported('cast `${name}` with ${call_args.len} arguments') + } + } else { + return g.unsupported('unresolved function call `${name}`') + } + i = call_end + 1 + } +} + +fn fastc_matching_rpar(tokens []FastcExpressionToken, open int) ?int { + mut depth := 0 + for i in open .. tokens.len { + match tokens[i].tok { + .lpar { + depth++ + } + .rpar { + depth-- + if depth == 0 { + return i + } + } + else {} + } + } + return none +} + +fn fastc_call_arguments(tokens []FastcExpressionToken, open int, close int) ![][]FastcExpressionToken { + if open + 1 == close { + return [][]FastcExpressionToken{} + } + mut call_args := [][]FastcExpressionToken{} + mut start := open + 1 + mut depth := 0 + for i in open + 1 .. close { + match tokens[i].tok { + .lpar { + depth++ + } + .rpar { + depth-- + } + .comma { + if depth == 0 { + if start == i { + return error('empty fastc function argument') + } + call_args << tokens[start..i] + start = i + 1 + } + } + else {} + } + } + if start == close { + return error('empty fastc function argument') + } + call_args << tokens[start..close] + return call_args +} + +fn (g &Parser) infer_expression_type(tokens []FastcExpressionToken) !string { + if tokens.len == 0 { + return '' + } + mut start := 0 + mut end := tokens.len + for end - start >= 2 && tokens[start].tok == .lpar { + wrapper_end := fastc_matching_rpar(tokens[start..end], 0) or { break } + if wrapper_end != end - start - 1 { + break + } + start++ + end-- + } + if start >= end { + return '' + } + if end - start == 1 { + item := tokens[start] + return match item.tok { + .name { + if local := g.locals[item.lit] { + local.typ + } else { + '' + } + } + .number { + fastc_number_expression_type(item.lit) + } + .string { + 'string' + } + .key_true, .key_false { + 'bool' + } + .key_nil { + 'nil' + } + else { + '' + } + } + } + if tokens[start].tok == .name && start + 1 < end && tokens[start + 1].tok == .lpar { + if close := fastc_matching_rpar(tokens[start..end], 1) { + if close == end - start - 1 { + name := tokens[start].lit + if signature := g.functions[name] { + return signature.return_type + } + if primitive := fastc_primitive_c_type(name) { + return primitive + } + return '' + } + } + } + if tokens[start].tok in [.plus, .minus] { + operand_type := g.infer_expression_type(tokens[start + 1..end])! + if !fastc_is_numeric_expression_type(operand_type) { + return g.unsupported('arithmetic `${tokens[start].tok.str()}` on non-numeric type `${operand_type}`') + } + return operand_type + } + if tokens[start].tok == .bit_not { + operand_type := g.infer_expression_type(tokens[start + 1..end])! + if !fastc_is_integer_expression_type(operand_type) { + return g.unsupported('bitwise negation of non-integer type `${operand_type}`') + } + return operand_type + } + if tokens[end - 1].tok in [.inc, .dec] { + operand_type := g.infer_expression_type(tokens[start..end - 1])! + if !fastc_is_numeric_expression_type(operand_type) { + return g.unsupported('arithmetic `${tokens[end - 1].tok.str()}` on non-numeric type `${operand_type}`') + } + return operand_type + } + mut depth := 0 + for i in start .. end { + match tokens[i].tok { + .lpar { depth++ } + .rpar { depth-- } + else {} + } + if depth != 0 { + continue + } + if tokens[i].tok.is_assignment() { + return g.infer_expression_type(tokens[start..i])! + } + if tokens[i].tok in [.plus, .minus, .mul, .amp, .pipe, .xor] && i > start { + left_type := g.infer_expression_type(tokens[start..i])! + right_type := g.infer_expression_type(tokens[i + 1..end])! + common_type := fastc_common_arithmetic_type(left_type, right_type) + if common_type.len == 0 { + return g.unsupported('arithmetic `${tokens[i].tok.str()}` operands of types `${left_type}` and `${right_type}`') + } + return common_type + } + } + return '' +} + +fn fastc_number_expression_type(literal string) string { + clean := literal.replace('_', '') + if clean.contains('.') || (!(clean.starts_with('0x') || clean.starts_with('0X')) + && clean.contains_any('eE')) { + return 'float literal' + } + return 'integer literal' +} + +fn fastc_common_arithmetic_type(left string, right string) string { + if left == right && fastc_is_numeric_expression_type(left) { + return left + } + if left == 'integer literal' && fastc_is_integer_type(right) { + return right + } + if right == 'integer literal' && fastc_is_integer_type(left) { + return left + } + if left == 'float literal' && right in ['f32', 'f64'] { + return right + } + if right == 'float literal' && left in ['f32', 'f64'] { + return left + } + return '' +} + +fn fastc_is_numeric_expression_type(typ string) bool { + return typ in ['integer literal', 'float literal', 'f32', 'f64'] || fastc_is_integer_type(typ) +} + +fn fastc_is_integer_expression_type(typ string) bool { + return typ == 'integer literal' || fastc_is_integer_type(typ) +} + +fn fastc_call_types_are_compatible(actual string, expected string) bool { + if actual == expected { + return true + } + if actual == 'integer literal' { + return fastc_is_integer_type(expected) + } + if actual == 'float literal' { + return expected in ['f32', 'f64'] + } + if actual == 'nil' { + return expected.ends_with('*') || expected in ['voidptr', 'byteptr', 'charptr'] + } + return false +} + +fn fastc_is_integer_type(typ string) bool { + return typ in ['byte', 'char', 'i8', 'i16', 'i32', 'i64', 'int', 'isize', 'rune', 'u8', 'u16', + 'u32', 'u64', 'unsigned int', 'usize'] +} + +fn fastc_nondecimal_literal_is_type_sensitive(literal string) bool { + clean := literal.replace('_', '') + if clean.len <= 2 || clean[0] != `0` { + return false + } + digits := clean[2..].trim_left('0') + if clean[1] in [`x`, `X`] { + if digits.len > 8 { + return true + } + return digits.len == 8 && ((digits[0] >= `8` && digits[0] <= `9`) + || (digits[0] >= `a` && digits[0] <= `f`) + || (digits[0] >= `A` && digits[0] <= `F`)) + } + if clean[1] in [`b`, `B`] { + return digits.len >= 32 + } + if clean[1] in [`o`, `O`] { + return digits.len > 11 || (digits.len == 11 && digits[0] >= `2`) + } + return false +} + +fn fastc_decimal_literal_is_type_sensitive(literal string) bool { + clean := literal.replace('_', '') + if clean.len == 0 || clean.contains_any('.eE') { + return false + } + for digit in clean { + if !digit.is_digit() { + return false + } + } + digits := clean.trim_left('0') + int_max_literal := '2147483647' + if digits.len != int_max_literal.len { + return digits.len > int_max_literal.len + } + for i in 0 .. digits.len { + if digits[i] != int_max_literal[i] { + return digits[i] > int_max_literal[i] + } + } + return false +} + +fn fastc_c_number(literal string) !string { + clean := literal.replace('_', '') + if fastc_decimal_literal_is_type_sensitive(literal) { + // C assigns oversized decimal tokens a wider type before any surrounding + // operation. Reject them until the direct parser can preserve V inference. + return error('fastc parser does not support oversized decimal literal expressions') + } + if fastc_nondecimal_literal_is_type_sensitive(literal) { + return error('fastc parser does not support high-bit nondecimal literals') + } + if clean.len > 2 && clean[0] == `0` && clean[1] in [`o`, `O`] { + // V spells octal integers with an explicit 0o prefix. GNU C uses a + // leading zero, so translate the prefix before emitting the token. + return '0' + clean[2..] + } + if clean.len < 2 || clean[0] != `0` || !clean[1].is_digit() || clean.contains_any('.eE') { + return clean + } + mut first_digit := 0 + for first_digit < clean.len - 1 && clean[first_digit] == `0` { + first_digit++ + } + return clean[first_digit..] +} + +fn (g &Parser) token_source() string { + if g.lit.len > 0 { + return g.lit + } + return g.tok.str() +} + +fn (mut g Parser) write_line(line string) { + for _ in 0 .. g.indent { + g.out.write_u8(`\t`) + } + g.out.writeln(line) +} + +fn fastc_needs_space(last u8, next string) bool { + if next.len == 0 { + return false + } + return (last.is_alnum() || last == `_`) && (next[0].is_alnum() || next[0] == `_`) +} + +fn fastc_c_string(literal string) !string { + if literal.len < 2 { + return error('invalid fastc string literal') + } + mut raw := literal + mut is_raw := false + if raw[0] == `r` && raw.len >= 3 { + is_raw = true + raw = raw[1..] + } + quote := raw[0] + if quote !in [`'`, `"`] || raw[raw.len - 1] != quote { + return error('interpolated or unfinished fastc string literal') + } + content := raw[1..raw.len - 1] + if fastc_string_contains_nul(content, is_raw) { + return error('fastc parser does not support embedded NUL string literals') + } + mut result := strings.new_builder(raw.len + 2) + result.write_u8(`"`) + mut i := 1 + for i < raw.len - 1 { + c := raw[i] + if c == `\\` && !is_raw && i + 1 < raw.len - 1 { + if raw[i + 1] == `\n` { + i += 2 + for i < raw.len - 1 && raw[i] in [` `, `\t`, `\r`] { + i++ + } + continue + } + if raw[i + 1] == `\r` && i + 2 < raw.len - 1 && raw[i + 2] == `\n` { + i += 3 + for i < raw.len - 1 && raw[i] in [` `, `\t`] { + i++ + } + continue + } + if raw[i + 1] == `x` { + if i + 3 >= raw.len - 1 { + return error('invalid fastc hex escape') + } + high := fastc_hex_digit_value(raw[i + 2])! + low := fastc_hex_digit_value(raw[i + 3])! + value := (high << 4) | low + // V consumes exactly two hexadecimal digits. C consumes every + // following hex digit, so use a full three-digit octal escape to + // terminate the encoded byte unambiguously. + result.write_u8(`\\`) + result.write_u8(`0` + (value >> 6)) + result.write_u8(`0` + ((value >> 3) & 7)) + result.write_u8(`0` + (value & 7)) + i += 4 + continue + } + if raw[i + 1] >= `0` && raw[i + 1] <= `7` && (i + 3 >= raw.len - 1 + || raw[i + 2] < `0` || raw[i + 2] > `7` || raw[i + 3] < `0` + || raw[i + 3] > `7`) { + // V only decodes three-digit octal escapes. Preserve a shorter + // spelling as a literal backslash and digits instead of letting C + // consume it as a one- or two-digit octal escape. + result.write_string('\\\\') + i++ + continue + } + result.write_u8(c) + result.write_u8(raw[i + 1]) + i += 2 + continue + } else if c == `"` { + result.write_string('\\"') + } else if c == `\\` && is_raw { + result.write_string('\\\\') + } else { + result.write_u8(c) + } + i++ + } + result.write_u8(`"`) + return result.str() +} + +fn fastc_hex_digit_value(c u8) !u8 { + if c >= `0` && c <= `9` { + return u8(c - `0`) + } + if c >= `a` && c <= `f` { + return u8(c - `a` + 10) + } + if c >= `A` && c <= `F` { + return u8(c - `A` + 10) + } + return error('invalid fastc hex digit `${c.ascii_str()}`') +} + +fn fastc_string_contains_nul(content string, is_raw bool) bool { + if content.bytes().contains(u8(0)) { + return true + } + if is_raw { + return false + } + mut i := 0 + for i + 1 < content.len { + if content[i] != `\\` { + i++ + continue + } + escape := content[i + 1] + if escape == `\\` { + i += 2 + continue + } + if escape >= `0` && escape <= `7` && i + 3 < content.len && content[i + 2] >= `0` + && content[i + 2] <= `7` && content[i + 3] >= `0` && content[i + 3] <= `7` { + high := int(escape - `0`) + middle := int(content[i + 2] - `0`) + low := int(content[i + 3] - `0`) + value := high * 64 + middle * 8 + low + // V stores three-digit octal escapes in a byte, including wrapping + // values such as \400 to NUL. + if u8(value) == 0 { + return true + } + i += 4 + continue + } + if escape == `0` + || (escape == `x` && i + 3 < content.len && content[i + 2..i + 4] == '00') + || (escape == `u` && i + 5 < content.len && content[i + 2..i + 6] == '0000') + || (escape == `U` && i + 9 < content.len && content[i + 2..i + 10] == '00000000') { + return true + } + i += 2 + } + return false +} diff --git a/vlib/v3/gen/fastc/fastc_test.v b/vlib/v3/gen/fastc/fastc_test.v new file mode 100644 index 00000000000000..db37b1694edbc1 --- /dev/null +++ b/vlib/v3/gen/fastc/fastc_test.v @@ -0,0 +1,649 @@ +module fastc + +import os +import v3.cmdexec +import v3.pref + +fn test_generate_and_compile_without_flat_ast() { + source := 'module main + +fn main() { + mut total := 0 + label := "total=" + for i in 0 .. 3 { + total += twice(i) + } + if true { + print(label) + println(total) + } else { + println(0) + } +} + +fn twice(value int) int { + return value * 2 +} +' + prefs := pref.new_preferences() + c_source := generate(source, 'fastc_test.v', prefs) or { panic(err) } + assert c_source.contains('__typeof__((0)) total = (0);') + assert c_source.contains('string label = ("total=");') + assert c_source.contains('__v_fastc_range_start_0 = (0);') + assert c_source.contains('__v_fastc_range_end_1 = (3);') + assert c_source.contains('int twice(int value);') + assert c_source.contains('setvbuf(stdout, NULL, _IONBF, 0);') + assert !c_source.contains('v3.flat') + + root := os.join_path(os.vtmp_dir(), 'v3_fastc_${os.getpid()}') + os.rmdir_all(root) or {} + os.mkdir_all(root) or { panic(err) } + defer { + os.rmdir_all(root) or {} + } + c_file := os.join_path(root, 'program.c') + bin_file := os.join_path(root, 'program') + os.write_file(c_file, c_source) or { panic(err) } + tcc := os.join_path(prefs.vroot, 'thirdparty', 'tcc', 'tcc.exe') + compile_result := cmdexec.run(tcc, ['-std=gnu11', '-o', bin_file, c_file]) + assert compile_result.exit_code == 0, compile_result.output + run_result := cmdexec.run(bin_file, []) + assert run_result.exit_code == 0, run_result.output + assert run_result.output.trim_space() == 'total=6' +} + +fn test_top_level_statements_emit_main_directly() { + prefs := pref.new_preferences() + c_source := generate("println('Hello, World!')\n", 'hello_world.v', prefs) or { panic(err) } + assert c_source.contains('int main(void) {') + assert c_source.contains('println("Hello, World!");') + assert c_source.contains('setvbuf(stdout, NULL, _IONBF, 0);') +} + +fn test_unsupported_import_is_rejected() { + prefs := pref.new_preferences() + mut failed := false + _ := generate('module main\nimport os\nfn main() {}\n', 'imports.v', prefs) or { + failed = true + '' + } + assert failed +} + +fn test_unresolved_names_are_rejected_before_c_emission() { + prefs := pref.new_preferences() + for source in [ + "module main\nfn main() { puts('hello') }\n", + 'module main\nfn main() { printf("hello") }\n', + 'module main\nfn main() { value := stdout; println(value) }\n', + ] { + mut message := '' + _ := generate(source, 'unresolved_name.v', prefs) or { + message = err.msg() + '' + } + assert message.contains('fastc parser does not support unresolved name'), message + } +} + +fn test_declared_names_are_available_without_an_ast() { + prefs := pref.new_preferences() + c_source := generate('module main + +fn main() { + println(later(2)) +} + +fn later(value int) int { + return value + 1 +} +', + 'declared_names.v', prefs) or { panic(err) } + assert c_source.contains('println(later(2));') +} + +fn test_narrow_integer_cast_expressions_are_rejected() { + prefs := pref.new_preferences() + mut message := '' + _ := generate('module main + +fn main() { + println(u8(255) + u8(1)) +} +', + 'narrow_cast_expression.v', prefs) or { + message = err.msg() + '' + } + assert message.contains('narrow integer cast expressions'), message +} + +fn test_undeclared_function_signature_types_are_rejected() { + prefs := pref.new_preferences() + for source in [ + 'module main\nfn show(x size_t) { println(1) }\nfn main() { show(1) }\n', + 'module main\nfn value() size_t { return 1 }\nfn main() { println(value()) }\n', + ] { + mut message := '' + _ := generate(source, 'undeclared_signature_type.v', prefs) or { + message = err.msg() + '' + } + assert message.contains('undeclared type `size_t`'), message + } +} + +fn test_declared_function_call_argument_types_are_validated() { + prefs := pref.new_preferences() + mut message := '' + _ := generate('module main + +fn show(x bool) { + println(x) +} + +fn main() { + show(2) +} +', + 'invalid_call_argument.v', prefs) or { + message = err.msg() + '' + } + assert message.contains('argument 1 of type `integer literal`'), message + assert message.contains('function `show` expecting `bool`'), message + + c_source := generate('module main + +fn increment(x int) int { + return x + 1 +} + +fn show(x bool) { + println(x) +} + +fn main() { + value := 2 + flag := true + println(increment(value)) + show(flag) +} +', + 'valid_call_arguments.v', prefs) or { panic(err) } + assert c_source.contains('println(increment(value));') + assert c_source.contains('show(flag);') +} + +fn test_scanner_diagnostics_are_rejected() { + prefs := pref.new_preferences() + source := "module main\nfn main() { println('" + r'\_' + "') }\n" + mut message := '' + _ := generate(source, 'scanner_diagnostic.v', prefs) or { + message = err.msg() + '' + } + assert message.contains('fastc scanner error'), message + assert message.contains('`_` unknown escape sequence'), message +} + +fn test_conditions_must_be_boolean() { + prefs := pref.new_preferences() + for source in [ + 'module main\nfn main() { if 2 { println(1) } }\n', + 'module main\nfn main() { value := 2; for value { break } }\n', + 'module main\nfn main() { for 2 { break } }\n', + 'module main\nfn main() { for i := 0; 2; i++ { break } }\n', + ] { + mut message := '' + _ := generate(source, 'non_boolean_condition.v', prefs) or { + message = err.msg() + '' + } + assert message.contains('condition of type'), message + assert message.contains('instead of `bool`'), message + } + + c_source := generate('module main + +fn ready() bool { + return true +} + +fn main() { + flag := true + if flag { + println(1) + } + for ready() { + break + } + for i := 0; ready(); i++ { + break + } +} +', + 'boolean_conditions.v', prefs) or { panic(err) } + assert c_source.contains('if (flag) {') + assert c_source.contains('while (ready()) {') + assert c_source.contains('; ready(); i++) {') +} + +fn test_return_expression_type_is_validated() { + prefs := pref.new_preferences() + for source in [ + 'module main\nfn value() bool { return 2 }\nfn main() { println(value()) }\n', + 'module main\nfn value() int { return true }\nfn main() { println(value()) }\n', + ] { + mut message := '' + _ := generate(source, 'invalid_return_type.v', prefs) or { + message = err.msg() + '' + } + assert message.contains('return expression of type'), message + assert message.contains('function returning'), message + } + + c_source := generate('module main + +fn enabled() bool { + return true +} + +fn value() int { + return 2 +} + +fn main() { + println(enabled()) + println(value()) +} +', + 'valid_return_types.v', prefs) or { panic(err) } + assert c_source.contains('return ((bool)true);') + assert c_source.contains('return 2;') +} + +fn test_assignment_value_type_is_validated() { + prefs := pref.new_preferences() + for source in [ + 'module main\nfn main() { mut enabled := false; enabled = 2; println(enabled) }\n', + 'module main\nfn main() { mut count := 1; count = true; println(count) }\n', + 'module main\nfn main() { mut enabled := false; enabled += 1; println(enabled) }\n', + ] { + mut message := '' + _ := generate(source, 'invalid_assignment_type.v', prefs) or { + message = err.msg() + '' + } + assert message.contains('assignment of type'), message + assert message.contains('of type'), message + } + + c_source := generate('module main + +fn ready() bool { + return true +} + +fn main() { + mut enabled := false + enabled = ready() + mut count := 1 + count = 2 + count += 3 + println(enabled) + println(count) +} +', + 'valid_assignment_types.v', prefs) or { panic(err) } + assert c_source.contains('enabled=ready();') + assert c_source.contains('count=2;') + assert c_source.contains('count+=3;') +} + +fn test_main_must_not_return_a_value() { + prefs := pref.new_preferences() + mut message := '' + _ := generate('module main\nfn main() int { return 7 }\n', 'value_returning_main.v', prefs) or { + message = err.msg() + '' + } + assert message.contains('main function returning `int`'), message +} + +fn test_main_must_not_have_parameters() { + prefs := pref.new_preferences() + for source in [ + 'module main\nfn main(code int) {}\n', + 'module main\nfn main(code int) int { return code }\n', + ] { + mut message := '' + _ := generate(source, 'parameterized_main.v', prefs) or { + message = err.msg() + '' + } + assert message.contains('main function with parameters'), message + } +} + +fn test_range_bounds_must_be_integers() { + prefs := pref.new_preferences() + for source in [ + 'module main\nfn main() { for i in 0.0 .. 2.0 { println(i) } }\n', + 'module main\nfn main() { for i in 0 .. 2.0 { println(i) } }\n', + 'module main\nfn main() { for i in false .. true { println(i) } }\n', + ] { + mut message := '' + _ := generate(source, 'invalid_range_bounds.v', prefs) or { + message = err.msg() + '' + } + assert message.contains('range bounds of types'), message + assert message.contains('must both be integers'), message + } +} + +fn test_arithmetic_operands_must_be_numeric() { + prefs := pref.new_preferences() + for source in [ + 'module main\nfn main() { println(true + false) }\n', + 'module main\nfn main() { value := true * false; println(value) }\n', + 'module main\nfn main() { mut value := true; value += false; println(value) }\n', + ] { + mut message := '' + _ := generate(source, 'non_numeric_arithmetic.v', prefs) or { + message = err.msg() + '' + } + assert message.contains('arithmetic'), message + assert message.contains('non-numeric') || message.contains('operands of types'), message + } +} + +fn test_nil_requires_an_unsafe_block() { + prefs := pref.new_preferences() + mut message := '' + _ := generate('module main\nfn show(p &int) { println(*p) }\nfn main() { show(nil) }\n', + 'nil_outside_unsafe.v', prefs) or { + message = err.msg() + '' + } + assert message.contains('`nil` outside an `unsafe` block'), message + + c_source := generate('module main\nfn accept(p &int) {}\nfn main() { unsafe { accept(nil) } }\n', + 'nil_inside_unsafe.v', prefs) or { panic(err) } + assert c_source.contains('accept(NULL);') +} + +fn test_bitwise_negation_requires_an_integer() { + prefs := pref.new_preferences() + mut message := '' + _ := generate('module main\nfn main() { println(~true) }\n', 'bool_bit_not.v', prefs) or { + message = err.msg() + '' + } + assert message.contains('bitwise negation of non-integer type `bool`'), message + + c_source := generate('module main\nfn main() { println(~1) }\n', 'integer_bit_not.v', prefs) or { + panic(err) + } + assert c_source.contains('println(~1);') +} + +fn test_value_only_expression_statements_are_rejected() { + prefs := pref.new_preferences() + for source in [ + 'module main\nfn main() { 1 }\n', + 'module main\nfn main() { true }\n', + 'module main\nfn main() { value := 1; value }\n', + 'module main\nfn main() { int(1) }\n', + ] { + mut message := '' + _ := generate(source, 'value_expression_statement.v', prefs) or { + message = err.msg() + '' + } + assert message.contains('value-only expression statement'), message + } + + c_source := generate('module main\nfn touch() {}\nfn main() { mut count := 0; touch(); count++ }\n', + 'valid_expression_statements.v', prefs) or { panic(err) } + assert c_source.contains('touch();') + assert c_source.contains('count++;') +} + +fn test_bare_return_from_main_emits_zero() { + prefs := pref.new_preferences() + c_source := generate('module main + +fn stop() { + return +} + +fn main() { + if true { + return + } +} +', + 'bare_return.v', prefs) or { panic(err) } + assert c_source.contains('void stop(void) {\n\treturn;\n}') + assert c_source.contains('if (((bool)true)) {\n\t\treturn 0;\n\t}') +} + +fn test_non_void_functions_must_return_on_every_path() { + prefs := pref.new_preferences() + for source in [ + 'module main\nfn value() int {}\nfn main() { println(value()) }\n', + 'module main\nfn value() int { if true { return 1 } }\nfn main() { println(value()) }\n', + 'module main\nfn value() int { return }\nfn main() { println(value()) }\n', + ] { + mut message := '' + _ := generate(source, 'non_void_fallthrough.v', prefs) or { + message = err.msg() + '' + } + assert message.contains('fastc parser does not support'), message + } + c_source := generate('module main + +fn value(flag bool) int { + if flag { + return 1 + } else { + return 2 + } +} + +fn main() { + println(value(true)) +} +', + 'non_void_returns.v', prefs) or { panic(err) } + assert c_source.contains('return 1;') + assert c_source.contains('return 2;') +} + +fn test_integer_range_caches_bounds() { + prefs := pref.new_preferences() + c_source := generate('module main + +fn start() int { + return 0 +} + +fn limit() int { + return 3 +} + +fn main() { + for i in start() .. limit() { + println(i) + } +} +', + 'range_bounds.v', prefs) or { panic(err) } + assert c_source.contains('__v_fastc_range_start_0 = (start());') + assert c_source.contains('__v_fastc_range_end_1 = (limit());') + assert c_source.contains('i < (__v_fastc_range_end_1)') + assert !c_source.contains('i < (limit())') +} + +fn test_decimal_literals_preserve_v_values() { + prefs := pref.new_preferences() + c_source := generate('module main + +fn main() { + println(0_123) +} +', 'literal_values.v', prefs) or { + panic(err) + } + assert c_source.contains('println(123);') +} + +fn test_v_octal_literals_are_translated_to_gnu_c() { + assert fastc_c_number('0o17')! == '017' + assert fastc_c_number('0O7_1')! == '071' + mut oversized_message := '' + _ := fastc_c_number('0o20000000000') or { + oversized_message = err.msg() + '' + } + assert oversized_message.contains('high-bit nondecimal literals') + prefs := pref.new_preferences() + c_source := generate('module main + +fn main() { + println(0o17) +} +', 'octal_literal.v', prefs) or { + panic(err) + } + assert c_source.contains('println(017);') +} + +fn test_hex_string_escape_has_fixed_width_in_c() { + prefs := pref.new_preferences() + c_source := generate("module main\nfn main() { println('\\x61ardvark') }\n", 'hex_escape.v', + prefs) or { panic(err) } + assert c_source.contains(r'println("\141ardvark");') +} + +fn test_partial_octal_string_escapes_are_reencoded() { + assert fastc_c_string(r"'\1'")! == r'"\\1"' + assert fastc_c_string(r"'\12'")! == r'"\\12"' + assert fastc_c_string(r"'\123'")! == r'"\123"' +} + +fn test_string_line_continuations_match_v_unescaping() { + prefs := pref.new_preferences() + source := r"module main + +fn main() { + println('left\ + right') +} +" + c_source := generate(source, 'continued_string.v', prefs) or { panic(err) } + assert c_source.contains(r'println("leftright");') + crlf_literal := "'left\\" + '\r\n' + "\t right'" + assert fastc_c_string(crlf_literal)! == '"leftright"' + assert fastc_c_string(r"'left\nright'")! == r'"left\nright"' +} + +fn test_runtime_sensitive_constructs_are_rejected() { + prefs := pref.new_preferences() + for source in ['module main + +fn main() { + println("a\\0b") +} +', + "module main\nfn main() { println('\\400tail') }\n"] { + mut nul_failed := false + _ := generate(source, 'nul_string.v', prefs) or { + nul_failed = true + '' + } + assert nul_failed + } + assert fastc_string_contains_nul(r'\400tail', false) + assert !fastc_string_contains_nul(r'\401tail', false) + non_nul_octal_c := generate("module main\nfn main() { println('\\401tail') }\n", + 'non_nul_octal_string.v', prefs) or { panic(err) } + assert non_nul_octal_c.contains(r'println("\401tail");') + + mut assert_failed := false + _ := generate('module main + +fn main() { + assert false +} +', 'assert.v', prefs) or { + assert_failed = true + '' + } + assert assert_failed +} + +fn test_type_sensitive_expressions_are_rejected() { + prefs := pref.new_preferences() + for source in [ + 'module main\nfn main() { println(1 == 1) }\n', + 'module main\nfn main() { println(!false) }\n', + 'module main\nfn show(a u8, b u8) { println(a + b) }\nfn main() { show(255, 1) }\n', + 'module main\nfn show(x int, n int) { println(x << n) }\nfn main() { show(1, 32) }\n', + 'module main\nfn shift(n int) { mut x := 1; x <<= n; println(x) }\nfn main() { shift(32) }\n', + 'module main\nfn shift(n int) { mut x := 1; x >>= n; println(x) }\nfn main() { shift(32) }\n', + 'module main\nfn shift(n int) { mut x := 1; x >>>= n; println(x) }\nfn main() { shift(32) }\n', + 'module main\nfn divide(a int, b int) int { return a / b }\nfn main() { println(divide(1, 0)) }\n', + 'module main\nfn modulo(a int, b int) int { return a % b }\nfn main() { println(modulo(1, 0)) }\n', + 'module main\nfn divide(b int) { mut x := 1; x /= b; println(x) }\nfn main() { divide(0) }\n', + 'module main\nfn modulo(b int) { mut x := 1; x %= b; println(x) }\nfn main() { modulo(0) }\n', + 'module main\nfn main() { println(sizeof(string)) }\n', + "module main\nfn main() { s := 'abc'; println(s[0]) }\n", + "module main\nfn main() { println(c'a') }\n", + 'module main\nfn main() { println(`A`) }\n', + 'module main\nfn show(r rune) { println(r) }\nfn main() { show(65) }\n', + 'module main\nfn main() { println(rune(65)) }\n', + 'module main\nfn show(p charptr) { println(p) }\nfn main() { unsafe { show(nil) } }\n', + 'module main\nfn main() { p := charptr(0); println(p) }\n', + 'module main\nfn main() { println(1 ^ 2 + 3) }\n', + 'module main\nfn main() { println(10 & 3 + 1) }\n', + 'module main\nfn main() { println(1 | 2 ^ 3) }\n', + 'module main\nfn main() { println(1 & 2 * 3) }\n', + 'module main\nfn main() { mut x := -2_147_483_648; x--; println(x) }\n', + 'module main\nfn main() { for i := -2_147_483_648; true; i-- { println(i); break } }\n', + 'module main\nfn main() { mut x := -2_147_483_648 - 1; println(x) }\n', + 'module main\nfn main() { x := 2_147_483_649 | 0; println(x) }\n', + 'module main\nfn main() { x := 0xffff_ffff | 0; println(x) }\n', + 'module main\nfn main() { x := 0b11111111111111111111111111111111 | 0; println(x) }\n', + 'module main\nfn main() { mut a := 1; mut b := 2; a, b = b, a; println(a); println(b) }\n', + ] { + mut failed := false + _ := generate(source, 'typed_expression.v', prefs) or { + failed = true + '' + } + assert failed + } + + bool_c := generate('module main\nfn main() { println(true) }\n', 'bool_literal.v', prefs) or { + panic(err) + } + assert bool_c.contains('println(((bool)true));') + low_hex_c := generate('module main\nfn main() { x := 0x7fff_ffff | 0; println(x) }\n', + 'low_hex_literal.v', prefs) or { panic(err) } + assert low_hex_c.contains('__typeof__((0x7fffffff|0)) x = (0x7fffffff|0);') + low_binary_c := generate('module main\nfn main() { x := 0b01111111111111111111111111111111 | 0; println(x) }\n', + 'low_binary_literal.v', prefs) or { panic(err) } + assert low_binary_c.contains('__typeof__((0b01111111111111111111111111111111|0))') + max_int_c := generate('module main\nfn main() { x := 2_147_483_647 - 1; println(x) }\n', + 'max_int_expression.v', prefs) or { panic(err) } + assert max_int_c.contains('__typeof__((2147483647-1)) x = (2147483647-1);') + call_c := generate('module main\nfn sum(a int, b int) int { return a + b }\nfn main() { println(sum(1, 2)) }\n', + 'call_comma.v', prefs) or { panic(err) } + assert call_c.contains('println(sum(1,2));') +} diff --git a/vlib/v3/tests/fastc_backend_test.v b/vlib/v3/tests/fastc_backend_test.v new file mode 100644 index 00000000000000..eac9f2d706e67a --- /dev/null +++ b/vlib/v3/tests/fastc_backend_test.v @@ -0,0 +1,203 @@ +import os +import v3.cmdexec + +const fastc_backend_v3_dir = os.dir(os.dir(@FILE)) +const fastc_backend_vlib_dir = os.dir(fastc_backend_v3_dir) +const fastc_backend_v3_source = os.join_path(fastc_backend_v3_dir, 'v3.v') + +struct UnsupportedFastCInvocation { + args []string + expected string +} + +fn write_fastc_test_source(path string, source string) { + os.write_file(path, source) or { panic(err) } +} + +fn test_fastc_backend_parses_directly_to_c_without_ast_fallback() { + root := os.join_path(os.vtmp_dir(), 'v3_fastc_backend_${os.getpid()}') + os.rmdir_all(root) or {} + os.mkdir_all(root) or { panic(err) } + defer { + os.rmdir_all(root) or {} + } + v3_bin := os.join_path(root, 'v3') + build := cmdexec.run(@VEXE, ['-gc', 'none', '-path', '${fastc_backend_vlib_dir}|@vlib|@vmodules', + '-o', v3_bin, fastc_backend_v3_source]) + assert build.exit_code == 0, build.output + + valid_source := os.join_path(root, 'valid.v') + write_fastc_test_source(valid_source, 'module main + +fn twice(value int) int { + return value * 2 +} + +fn main() { + value := twice(21) + println(value) + println(0o17) +} +') + valid_binary := os.join_path(root, 'valid') + valid_compile := cmdexec.run(v3_bin, ['-macos-v3-compat-c99', '-b', 'fastc', '-o', valid_binary, + valid_source]) + assert valid_compile.exit_code == 0, valid_compile.output + assert valid_compile.output.contains('fastc parse+gen'), valid_compile.output + assert !valid_compile.output.contains(' check'), valid_compile.output + assert !valid_compile.output.contains(' transform'), valid_compile.output + assert !valid_compile.output.contains('markused'), valid_compile.output + retained_c := os.read_file(valid_binary + '.c') or { panic(err) } + assert retained_c.contains('__typeof__((twice(21))) value = (twice(21));') + assert retained_c.contains('println(017);') + assert retained_c.contains('setvbuf(stdout, NULL, _IONBF, 0);') + assert !retained_c.contains('builtin__builtin_init') + valid_run := cmdexec.run(valid_binary, []) + assert valid_run.exit_code == 0, valid_run.output + assert valid_run.output.trim_space() == '42\n15' + + cross_c := os.join_path(root, 'cross_linux.c') + cross_compile := cmdexec.run(v3_bin, ['-silent', '-b', 'fastc', '-os', 'linux', '-o', cross_c, + valid_source]) + assert cross_compile.exit_code == 0, cross_compile.output + cross_source := os.read_file(cross_c) or { panic(err) } + assert cross_source.contains('V_FASTC_PRINT_SELECT') + assert !cross_source.contains('builtin__builtin_init') + + run_c := os.join_path(root, 'run_output.c') + run_c_result := cmdexec.run(v3_bin, + ['-silent', '-b', 'fastc', '-o', run_c, 'run', valid_source]) + assert run_c_result.exit_code == 0, run_c_result.output + assert os.is_file(run_c) + assert !os.exists(run_c.all_before_last('.c')) + run_c_source := os.read_file(run_c) or { panic(err) } + assert run_c_source.contains('V_FASTC_PRINT_SELECT') + + run_stdout_result := cmdexec.run(v3_bin, ['-silent', '-b', 'fastc', '-o', '-', 'run', + valid_source]) + assert run_stdout_result.exit_code == 0, run_stdout_result.output + assert run_stdout_result.output.contains('V_FASTC_PRINT_SELECT') + assert !run_stdout_result.output.ends_with('42\n15\n') + + import_source := os.join_path(root, 'import.v') + write_fastc_test_source(import_source, 'module main + +import os + +fn main() { + println(os.args.len) +} +') + import_binary := os.join_path(root, 'import') + import_compile := cmdexec.run(v3_bin, ['-silent', '-b', 'fastc', '-o', import_binary, + import_source]) + assert import_compile.exit_code != 0 + assert import_compile.output.contains('fastc parser does not support top-level `import`'), import_compile.output + + assert !os.exists(import_binary) + assert !os.exists(import_binary + '.c') + + typed_source := os.join_path(root, 'typed.v') + write_fastc_test_source(typed_source, 'module main + +fn main() { + x := 2147483649 | 0 + println(x) +} +') + typed_compile := cmdexec.run(v3_bin, ['-silent', '-b', 'fastc', '-o', os.join_path(root, 'typed'), + typed_source]) + assert typed_compile.exit_code != 0 + assert typed_compile.output.contains('fastc parser does not support oversized decimal literal expressions'), typed_compile.output + + immutable_source := os.join_path(root, 'immutable.v') + write_fastc_test_source(immutable_source, 'module main + +fn main() { + value := 1 + value = 2 +} +') + immutable_compile := cmdexec.run(v3_bin, ['-silent', '-b', 'fastc', '-o', + os.join_path(root, 'immutable'), immutable_source]) + assert immutable_compile.exit_code != 0 + assert immutable_compile.output.contains('mutation of immutable or unknown name `value`'), immutable_compile.output + + invalid_c_source := os.join_path(root, 'invalid_c.v') + write_fastc_test_source(invalid_c_source, 'module main + +fn main() { + value := missing_name + println(value) +} +') + invalid_binary := os.join_path(root, 'invalid_c') + invalid_compile := cmdexec.run(v3_bin, ['-silent', '-b', 'fastc', '-o', invalid_binary, + invalid_c_source]) + assert invalid_compile.exit_code != 0 + assert invalid_compile.output.contains('missing_name'), invalid_compile.output + assert !os.exists(invalid_binary) + assert !os.exists(invalid_binary + '.c') + + preamble_name_source := os.join_path(root, 'preamble_name.v') + write_fastc_test_source(preamble_name_source, "module main + +fn main() { + puts('hello') +} +") + preamble_name_binary := os.join_path(root, 'preamble_name') + preamble_name_compile := cmdexec.run(v3_bin, ['-silent', '-b', 'fastc', '-o', + preamble_name_binary, preamble_name_source]) + assert preamble_name_compile.exit_code != 0 + assert preamble_name_compile.output.contains('fastc parser does not support unresolved name `puts`'), preamble_name_compile.output + + assert !os.exists(preamble_name_binary) + assert !os.exists(preamble_name_binary + '.c') + + fallthrough_source := os.join_path(root, 'fallthrough.v') + write_fastc_test_source(fallthrough_source, 'module main + +fn value() int {} + +fn main() { + println(value()) +} +') + fallthrough_binary := os.join_path(root, 'fallthrough') + fallthrough_compile := cmdexec.run(v3_bin, ['-silent', '-b', 'fastc', '-o', fallthrough_binary, + fallthrough_source]) + assert fallthrough_compile.exit_code != 0 + assert fallthrough_compile.output.contains('non-void function `value` that can fall through'), fallthrough_compile.output + + assert !os.exists(fallthrough_binary) + assert !os.exists(fallthrough_binary + '.c') + + for invocation in [ + UnsupportedFastCInvocation{ + args: ['-silent', '-prod', '-b', 'fastc', '-o', os.join_path(root, 'prod'), + valid_source] + expected: 'fastc parser does not support `-prod`' + }, + UnsupportedFastCInvocation{ + args: ['-silent', '-selfhost', '-b', 'fastc', '-o', os.join_path(root, 'selfhost'), + fastc_backend_v3_source] + expected: 'fastc parser does not support compiler self-hosting' + }, + UnsupportedFastCInvocation{ + args: ['-silent', '-b', 'fastc', '-d', 'no_main', '-o', os.join_path(root, + 'no_main.c'), + valid_source] + expected: 'fastc parser does not support `-d no_main`' + }, + UnsupportedFastCInvocation{ + args: ['-silent', '-autofree', '-b', 'fastc', '-o', os.join_path(root, 'autofree'), + valid_source] + expected: 'fastc parser does not support ownership/autofree' + }, + ] { + result := cmdexec.run(v3_bin, invocation.args) + assert result.exit_code != 0 + assert result.output.contains(invocation.expected), result.output + } +}