v / cmd / tools / vrepl.v
1088 lines · 1013 sloc · 28.85 KB · 8e64a34b0b975e6543ff94993108e2c6c2a1c1f8
Raw
1// Copyright (c) 2019-2024 Alexander Medvednikov. All rights reserved.
2// Use of this source code is governed by an MIT license
3// that can be found in the LICENSE file.
4module main
5
6import os
7import term
8import rand
9import readline
10import os.cmdline
11import v.util.version
12
13struct Repl {
14mut:
15 readline readline.Readline
16 indent int // indentation level
17 in_func bool // inside function decl
18 in_struct bool // inside struct decl
19 in_enum bool // inside enum decl
20 in_interface bool // inside interface decl
21 line string // the current line entered by the user
22 is_pin bool // does the repl 'pin' entered source code
23 folder string // the folder in which the repl will write its temporary source files
24 last_output string // the last repl output
25
26 modules map[string][]string // all the import modules
27 alias map[string]string // all the alias used in the import
28 includes []string // all the #include statements
29 functions []string // all the user function declarations
30 functions_name []string // all the user function names
31 structs []string // all the struct definitions
32 enums []string // all the enum definitions
33 consts []string // all the const definitions
34 types []string // all the type definitions
35 interfaces []string // all the interface definitions
36 lines []string // all the other lines/statements
37 exec_lines []string // executable statements with restored runtime values
38 temp_lines []string // all the temporary expressions/printlns
39 vstartup_lines []string // lines in the `VSTARTUP` file
40 eval_func_lines []string // same line of the `VSTARTUP` file, but used to test fn type
41}
42
43const is_stdin_a_pipe = os.is_atty(0) == 0
44const vexe = os.getenv('VEXE')
45const vquiet = os.getenv('VQUIET') != ''
46const vstartup = os.getenv('VSTARTUP')
47const repl_folder = os.join_path(os.vtmp_dir(), 'repl')
48
49const possible_statement_patterns = [
50 '++',
51 '--',
52 '/*',
53 'assert ',
54 'fn ',
55 'pub ',
56 'mut ',
57 'enum ',
58 'const ',
59 'struct ',
60 'interface ',
61 'import ',
62 '#include ',
63 'for ',
64 'if ',
65 'or ',
66 ' as ',
67]
68
69enum FnType {
70 none
71 void
72 fn_type
73}
74
75enum DeclType {
76 include // #include ...
77 const // const ...
78 type // type ...
79 enum // enum ...
80 fn // fn ...
81 struct // struct ...
82 interface // interface ...
83 stmt // statement
84}
85
86struct SnapshotAssignment {
87 lhs string
88 name string
89 op string
90}
91
92struct TimeSnapshot {
93 unix i64
94 nanosecond int
95 is_local bool
96}
97
98fn new_repl(folder string) Repl {
99 vstartup_source := os.read_file(vstartup) or { '' }.trim_right('\n\r').split_into_lines()
100 os.mkdir_all(folder) or {}
101 return Repl{
102 readline: readline.Readline{
103 skip_empty: true
104 }
105 folder: folder
106 modules: {
107 'os': []
108 'time': []
109 'math': []
110 }
111 vstartup_lines: vstartup_source
112 // Test file used to check if a function as a void return or a value return.
113 eval_func_lines: vstartup_source
114 }
115}
116
117fn endline_if_missed(line string) string {
118 if line.ends_with('\n') {
119 return line
120 }
121 return line + '\n'
122}
123
124fn starts_with_type_decl(line string, type_name string) bool {
125 if line.starts_with(type_name + ' ') || line.starts_with(type_name + '\t') {
126 return true
127 }
128 if line.starts_with('pub ') || line.starts_with('pub\t') {
129 substring := line[3..].trim_space()
130 if substring.starts_with(type_name + ' ') || substring.starts_with(type_name + '\t') {
131 return true
132 }
133 }
134 return false
135}
136
137fn repl_help() {
138 println(version.full_v_version(false))
139 println('
140 |help Displays this information.
141 |list Show the program so far.
142 |reset Clears the accumulated program, so you can start a fresh.
143 |Ctrl-C, Ctrl-D, exit Exits the REPL.
144 |clear Clears the screen.
145 |pin Pins the entered program to the top.
146 |!sh [COMMAND] Execute on REPL shell commands.
147'.strip_margin())
148}
149
150fn run_shell(command string) {
151 if command.len >= 2 && command[0..2] == 'cd' {
152 command_splited := command.split(' ')
153 assert command_splited.len >= 2
154 dir := command_splited[command_splited.len - 1]
155
156 os.chdir(dir) or { eprintln('`${command}` failed, err: ${err}') }
157 } else {
158 os.system(command)
159 }
160}
161
162fn (mut r Repl) checks() bool {
163 mut in_string := false
164 was_indent := r.indent > 0
165 for i := 0; i < r.line.len; i++ {
166 if r.line[i] == `'` && (i == 0 || r.line[i - 1] != `\\`) {
167 in_string = !in_string
168 }
169 if r.line[i] == `{` && !in_string {
170 r.line = r.line[..i + 1] + '\n' + r.line[i + 1..]
171 i++
172 r.indent++
173 }
174 if r.line[i] == `}` && !in_string {
175 r.line = r.line[..i] + '\n' + r.line[i..]
176 i++
177 r.indent--
178 if r.indent == 0 {
179 r.in_func = false
180 r.in_struct = false
181 r.in_enum = false
182 r.in_interface = false
183 }
184 }
185 }
186 return (was_indent && r.indent <= 0) || r.indent > 0
187}
188
189fn (r &Repl) function_call(line string) (bool, FnType) {
190 for function in r.functions_name {
191 is_function_definition := line.replace(' ', '').starts_with('${function}:=')
192 if line.starts_with(function) && !is_function_definition {
193 // TODO(vincenzopalazzo) store the type of the function here
194 fntype := r.check_fn_type_kind(line)
195 return true, fntype
196 }
197 }
198
199 if line.contains(':=') {
200 // an assignment to a variable:
201 // `z := abc()`
202 return false, FnType.none
203 }
204
205 // Check if it is a Vlib call
206 // TODO(vincenzopalazzo): auto import the module?
207 if r.is_function_call(line) {
208 fntype := r.check_fn_type_kind(line)
209 return true, fntype
210 }
211 return false, FnType.none
212}
213
214// TODO(vincenzopalazzo) Remove this fancy check and add a regex
215fn (r &Repl) is_function_call(line string) bool {
216 return !line.starts_with('[') && line.contains('.') && line.contains('(')
217 && (line.ends_with(')') || line.ends_with('?') || line.ends_with('!'))
218}
219
220// Convert the list of modules that we parsed already,
221// to a sequence of V source code lines
222fn (r &Repl) import_to_source_code() []string {
223 mut imports_line := []string{}
224 for mod, value in r.modules {
225 mut import_str := 'import ${mod}'
226 if mod in r.alias {
227 import_str += ' as ${r.alias[mod]}'
228 }
229 if value.len > 0 {
230 import_str += '{ '
231 for val in value {
232 import_str += '${val}, '
233 }
234 import_str += '}'
235 }
236 imports_line << endline_if_missed(import_str)
237 }
238 return imports_line
239}
240
241fn (r &Repl) current_source_code(should_add_temp_lines bool, not_add_print bool) string {
242 mut all_lines := r.import_to_source_code()
243
244 if vstartup != '' {
245 mut lines := []string{}
246 if !not_add_print {
247 lines = r.vstartup_lines.filter(!it.starts_with('print'))
248 } else {
249 lines = r.vstartup_lines.clone()
250 }
251 all_lines << lines
252 }
253 all_lines << r.includes
254 all_lines << r.types
255 all_lines << r.enums
256 all_lines << r.consts
257 all_lines << r.structs
258 all_lines << r.interfaces
259 all_lines << r.functions
260 all_lines << r.exec_lines
261
262 if should_add_temp_lines {
263 all_lines << r.temp_lines
264 }
265 return all_lines.join('\n')
266}
267
268fn (r &Repl) insert_source_code(typ DeclType, lines []string) string {
269 mut all_lines := r.import_to_source_code()
270
271 if vstartup != '' {
272 all_lines << r.vstartup_lines.filter(!it.starts_with('print'))
273 }
274 all_lines << r.includes
275 if typ == .include {
276 all_lines << lines
277 }
278 all_lines << r.types
279 if typ == .type {
280 all_lines << lines
281 }
282 all_lines << r.enums
283 if typ == .enum {
284 all_lines << lines
285 }
286 all_lines << r.consts
287 if typ == .const {
288 all_lines << lines
289 }
290 all_lines << r.structs
291 if typ == .struct {
292 all_lines << lines
293 }
294 all_lines << r.interfaces
295 if typ == .interface {
296 all_lines << lines
297 }
298 all_lines << r.functions
299 if typ == .fn {
300 all_lines << lines
301 }
302 all_lines << r.exec_lines
303 if typ == .stmt {
304 all_lines << lines
305 }
306 return all_lines.join('\n')
307}
308
309fn (r &Repl) current_display_source_code(should_add_temp_lines bool, not_add_print bool) string {
310 mut all_lines := r.import_to_source_code()
311 if vstartup != '' {
312 mut lines := []string{}
313 if !not_add_print {
314 lines = r.vstartup_lines.filter(!it.starts_with('print'))
315 } else {
316 lines = r.vstartup_lines.clone()
317 }
318 all_lines << lines
319 }
320 all_lines << r.includes
321 all_lines << r.types
322 all_lines << r.enums
323 all_lines << r.consts
324 all_lines << r.structs
325 all_lines << r.interfaces
326 all_lines << r.functions
327 all_lines << r.lines
328 if should_add_temp_lines {
329 all_lines << r.temp_lines
330 }
331 return all_lines.join('\n')
332}
333
334// the new_line is probably a function call, but some function calls
335// do not return anything, while others return results.
336// This function checks which one we have:
337fn (r &Repl) check_fn_type_kind(new_line string) FnType {
338 source_code := r.current_source_code(true, false) + '\nprintln(${new_line})'
339 check_file := os.join_path(r.folder, '${rand.ulid()}.vrepl.check.v')
340 os.write_file(check_file, source_code) or { panic(err) }
341 defer {
342 os.rm(check_file) or {}
343 }
344 // -usecache keeps repeated REPL checks responsive by reusing cached modules.
345 // -w suppresses warnings from this synthetic println probe.
346 // -check just does syntax and checker analysis without generating/running code.
347 os_response := execute_repl_v_command(vexe, ['-usecache', '-w', '-check', check_file]) or {
348 return FnType.none
349 }
350 str_response := convert_output(os_response.output)
351 if os_response.exit_code != 0 && str_response.contains('can not print void expressions') {
352 return FnType.void
353 }
354 return FnType.fn_type
355}
356
357// parse the import statement in `line`, updating the Repl alias maps
358fn (mut r Repl) parse_import(line string) {
359 if !line.contains('import') {
360 eprintln("the line doesn't contain an `import` keyword")
361 return
362 }
363 tokens := r.line.fields()
364 // module name
365 mod := tokens[1]
366 // set alias
367 if line.contains('as ') && tokens.len >= 4 {
368 alias := tokens[3]
369 if mod !in r.alias {
370 r.alias[mod] = alias
371 }
372 }
373
374 // set value
375 if line.contains('{') && line.contains('}') {
376 if mod !in r.modules {
377 r.modules[mod] = []string{}
378 }
379 values := line.split('{')[1].split('}')[0]
380 for value in values.split(',') {
381 r.modules[mod] << value
382 }
383 } else {
384 if mod !in r.modules {
385 r.modules[mod] = []string{}
386 }
387 }
388}
389
390// clear the screen, then list source code
391fn (mut r Repl) pin() {
392 term.erase_clear()
393 r.list_source()
394}
395
396// print source code
397fn (mut r Repl) list_source() {
398 source_code := r.current_display_source_code(true, true)
399 println('\n${source_code.replace('\n\n', '\n')}')
400}
401
402fn highlight_console_command(command string) string {
403 return term.bright_white(term.bright_bg_black(' ${command} '))
404}
405
406fn highlight_repl_command(command string) string {
407 return term.bright_white(term.bg_blue(' ${command} '))
408}
409
410fn print_welcome_screen() {
411 if vquiet {
412 return
413 }
414 cmd_exit := highlight_repl_command('exit')
415 cmd_list := highlight_repl_command('list')
416 cmd_help := highlight_repl_command('help')
417 cmd_v_help := highlight_console_command('v help')
418 cmd_v_run := highlight_console_command('v run main.v')
419 file_main := highlight_console_command('main.v')
420 vbar := term.bright_green('|')
421 width, _ := term.get_terminal_size() // get the size of the terminal
422 vlogo := [
423 term.bright_blue(r' ____ ____ '),
424 term.bright_blue(r' \ \ / / '),
425 term.bright_blue(r' \ \/ / '),
426 term.bright_blue(r' \ / '),
427 term.bright_blue(r' \ / '),
428 term.bright_blue(r' \__/ '),
429 ]
430 help_text := [
431 'Welcome to the V REPL (for help with V itself, type ${cmd_exit}, then run ${cmd_v_help}).',
432 'Note: the REPL is highly experimental. For best V experience, use a text editor, ',
433 'save your code in a ${file_main} file and execute: ${cmd_v_run}',
434 '${version.full_v_version(false)} . Use ${cmd_list} to see the accumulated program so far.',
435 'Use Ctrl-C or ${cmd_exit} to exit, or ${cmd_help} to see other available commands.',
436 ]
437 if width >= 97 {
438 eprintln('${vlogo[0]}')
439 eprintln('${vlogo[1]} ${vbar} ${help_text[0]}')
440 eprintln('${vlogo[2]} ${vbar} ${help_text[1]}')
441 eprintln('${vlogo[3]} ${vbar} ${help_text[2]}')
442 eprintln('${vlogo[4]} ${vbar} ${help_text[3]}')
443 eprintln('${vlogo[5]} ${vbar} ${help_text[4]}')
444 eprintln('')
445 } else {
446 if width >= 14 {
447 left_margin := ' '.repeat(int(width / 2 - 7))
448 for l in vlogo {
449 println(left_margin + l)
450 }
451 }
452 println(help_text.join('\n'))
453 }
454}
455
456fn remove_comment(oline string) string {
457 mut inside_string := false
458 mut escaped := false
459 mut maybe_comment := false
460
461 mut string_delim := 0
462
463 mut i := 0
464 for i < oline.len {
465 match oline[i] {
466 `"`, `'` {
467 if !escaped {
468 if inside_string && string_delim == oline[i] {
469 inside_string = false
470 } else if !inside_string {
471 inside_string = true
472 maybe_comment = false
473 string_delim = oline[i]
474 }
475 } else {
476 escaped = false
477 }
478 }
479 `\\` {
480 if inside_string {
481 escaped = !escaped
482 }
483 }
484 `/` {
485 if maybe_comment {
486 i--
487 break
488 } else if !inside_string {
489 maybe_comment = true
490 }
491 }
492 else {
493 escaped = false
494 maybe_comment = false
495 }
496 }
497
498 i++
499 }
500
501 return oline[..i]
502}
503
504fn is_repl_ident_start(ch u8) bool {
505 return (`a` <= ch && ch <= `z`) || (`A` <= ch && ch <= `Z`) || ch == `_`
506}
507
508fn is_repl_ident_char(ch u8) bool {
509 return is_repl_ident_start(ch) || (`0` <= ch && ch <= `9`)
510}
511
512fn is_repl_ident(name string) bool {
513 if name.len == 0 || !is_repl_ident_start(name[0]) {
514 return false
515 }
516 for i := 1; i < name.len; i++ {
517 if !is_repl_ident_char(name[i]) {
518 return false
519 }
520 }
521 return true
522}
523
524fn find_assignment_operator(line string) (int, string) {
525 mut inside_string := false
526 mut escaped := false
527 mut string_delim := u8(0)
528 for i := 0; i < line.len; i++ {
529 match line[i] {
530 `"`, `'` {
531 if !escaped {
532 if inside_string && string_delim == line[i] {
533 inside_string = false
534 } else if !inside_string {
535 inside_string = true
536 string_delim = line[i]
537 }
538 } else {
539 escaped = false
540 }
541 }
542 `\\` {
543 if inside_string {
544 escaped = !escaped
545 } else {
546 escaped = false
547 }
548 }
549 `:` {
550 if !inside_string && i + 1 < line.len && line[i + 1] == `=` {
551 return i, ':='
552 }
553 escaped = false
554 }
555 `=` {
556 prev_is_assignment_op := i > 0
557 && line[i - 1] in [`!`, `<`, `>`, `=`, `:`, `+`, `-`, `*`, `/`, `%`, `&`, `|`, `^`]
558 next_is_assignment_op := i + 1 < line.len && line[i + 1] == `=`
559 if !inside_string && !prev_is_assignment_op && !next_is_assignment_op {
560 return i, '='
561 }
562 escaped = false
563 }
564 else {
565 escaped = false
566 }
567 }
568 }
569 return -1, ''
570}
571
572fn parse_simple_assignment(line string) ?SnapshotAssignment {
573 idx, op := find_assignment_operator(line)
574 if idx < 1 {
575 return none
576 }
577 rhs_idx := idx + op.len
578 if rhs_idx >= line.len || line[rhs_idx..].trim_space().len == 0 {
579 return none
580 }
581 lhs := line[..idx].trim_space()
582 if lhs.len == 0 {
583 return none
584 }
585 mut name := lhs
586 if lhs.starts_with('mut ') {
587 name = lhs[4..].trim_space()
588 }
589 if name.len == 0 || name.contains_any(',.[({}: ') || !is_repl_ident(name) {
590 return none
591 }
592 return SnapshotAssignment{
593 lhs: lhs
594 name: name
595 op: op
596 }
597}
598
599fn repl_string_literal(s string) string {
600 escaped := s.replace_each(['\\', '\\\\', "'", "\\'", '\n', '\\n', '\r', '\\r', '\t', '\\t'])
601 return "'${escaped}'"
602}
603
604fn (mut r Repl) add_statement_line(display_line string, exec_line string) {
605 r.lines << display_line
606 r.exec_lines << exec_line
607}
608
609fn (mut r Repl) add_statement_lines(lines []string) {
610 r.lines << lines
611 r.exec_lines << lines
612}
613
614// Snapshot time.Time assignments so rerunning the accumulated source
615// keeps the original timestamp instead of calling time.now() again.
616fn (r &Repl) capture_time_snapshot(source_code string, assignment SnapshotAssignment) ?TimeSnapshot {
617 marker := '__vrepl_time_snapshot__${rand.ulid()}'
618 mut probe_source := source_code
619 probe_source += '\nprintln(${repl_string_literal(marker)} + \'\\t\' + typeof(${assignment.name}).name + \'\\t\' + ${assignment.name}.unix().str() + \'\\t\' + ${assignment.name}.nanosecond.str() + \'\\t\' + ${assignment.name}.is_local.str())\n'
620 probe_file := os.join_path(r.folder, '${rand.ulid()}.vrepl.time_snapshot.v')
621 os.write_file(probe_file, probe_source) or { return none }
622 defer {
623 os.rm(probe_file) or {}
624 }
625 result := repl_run_vfile(probe_file) or { return none }
626 if result.exit_code != 0 {
627 return none
628 }
629 lines := convert_output(result.output).trim_right('\n\r').split_into_lines()
630 prefix := '${marker}\t'
631 for i := lines.len - 1; i >= 0; i-- {
632 if !lines[i].starts_with(prefix) {
633 continue
634 }
635 parts := lines[i][prefix.len..].split('\t')
636 if parts.len != 4 || parts[0] != 'time.Time' {
637 return none
638 }
639 return TimeSnapshot{
640 unix: parts[1].i64()
641 nanosecond: parts[2].int()
642 is_local: parts[3] == 'true'
643 }
644 }
645 return none
646}
647
648fn snapshot_time_expr(snapshot TimeSnapshot) string {
649 base := 'time.unix_nanosecond(${snapshot.unix}, ${snapshot.nanosecond})'
650 if snapshot.is_local {
651 return base + '.as_local()'
652 }
653 return base + '.as_utc()'
654}
655
656fn (r &Repl) snapshot_assignment_line(source_code string, line string) string {
657 assignment := parse_simple_assignment(line) or { return line }
658 snapshot := r.capture_time_snapshot(source_code, assignment) or { return line }
659 return '${assignment.lhs} ${assignment.op} ${snapshot_time_expr(snapshot)}'
660}
661
662fn run_repl(workdir string, vrepl_prefix string) int {
663 os.mkdir_all(workdir) or {
664 rerror('Could not create the REPL working folder `${workdir}`: ${err}')
665 return 1
666 }
667 os.chdir(workdir) or {
668 rerror('Could not change the REPL working folder to `${workdir}`: ${err}')
669 return 1
670 }
671 if !is_stdin_a_pipe {
672 print_welcome_screen()
673 }
674
675 if vstartup != '' {
676 result := repl_run_vfile(vstartup) or {
677 os.Result{
678 output: '${vstartup} file not found'
679 }
680 }
681 print('\n')
682 print_output(result.output)
683 }
684 temp_file := os.join_path(workdir, '.${vrepl_prefix}vrepl_temp.v')
685 mut prompt := '>>> '
686 defer {
687 if !is_stdin_a_pipe {
688 println('')
689 }
690 cleanup_files(temp_file)
691 }
692 mut r := new_repl(workdir)
693
694 for {
695 if r.indent == 0 {
696 prompt = '>>> '
697 } else {
698 prompt = '... '
699 }
700
701 oline := r.get_one_line(prompt) or { break }
702 line := remove_comment(oline).trim_space()
703
704 if line == '' {
705 continue
706 }
707 if line.len <= -1 || line == 'exit' {
708 break
709 }
710 r.line = line
711 if r.line == 'clear' {
712 term.erase_clear()
713 continue
714 }
715 if r.line == 'help' {
716 repl_help()
717 continue
718 }
719
720 if r.line.len > 4 && r.line[0..3] == '!sh' {
721 run_shell(r.line[4..r.line.len])
722 continue
723 }
724
725 if r.line.contains(':=') && r.line.contains('fn(') {
726 r.in_func = true
727 r.functions_name << r.line.all_before(':= fn(').trim_space()
728 }
729
730 starts_with_fn := starts_with_type_decl(r.line, 'fn')
731 if starts_with_fn {
732 r.in_func = true
733 r.functions_name << r.line.all_after('fn').all_before('(').trim_space()
734 }
735 was_func := r.in_func
736
737 starts_with_struct := starts_with_type_decl(r.line, 'struct')
738 if starts_with_struct {
739 r.in_struct = true
740 }
741 was_struct := r.in_struct
742
743 starts_with_enum := starts_with_type_decl(r.line, 'enum')
744 if starts_with_enum {
745 r.in_enum = true
746 }
747 was_enum := r.in_enum
748
749 starts_with_interface := starts_with_type_decl(r.line, 'interface')
750 if starts_with_interface {
751 r.in_interface = true
752 }
753 was_interface := r.in_interface
754
755 if r.checks() {
756 for rline in r.line.split('\n') {
757 r.temp_lines << rline
758 }
759 if r.indent > 0 {
760 continue
761 }
762 r.line = ''
763 }
764 if r.line == 'debug_repl' {
765 eprintln('repl: ${r}')
766 continue
767 }
768 if r.line == 'reset' {
769 r = new_repl(workdir)
770 continue
771 }
772 if r.line == 'list' {
773 r.list_source()
774 continue
775 }
776 if r.line == 'pin' {
777 r.is_pin = !r.is_pin
778 if r.is_pin {
779 r.pin()
780 println('')
781 }
782 continue
783 }
784 if r.line.starts_with('=') {
785 r.line = 'println(' + r.line[1..] + ')'
786 }
787 if r.line.starts_with('print(') || r.line.starts_with('println(') {
788 // >>> println('hello')
789 source_code := r.current_source_code(false, false) + '\n${r.line}\n'
790 os.write_file(temp_file, source_code) or { panic(err) }
791 s := repl_run_vfile(temp_file) or { return 1 }
792 if s.output.len > r.last_output.len {
793 cur_line_output := s.output[r.last_output.len..]
794 print_output(cur_line_output)
795 if s.exit_code == 0 && !cur_line_output.contains('warning:') {
796 r.last_output = s.output.clone()
797 r.add_statement_line(r.line, r.line)
798 }
799 }
800 } else if r.line.contains('os.input(') {
801 // >>> s := os.input('name: ')
802 prompt_str := r.line.all_after('os.input(').all_before(')').trim('\'"')
803 line_t := r.get_one_line(prompt_str) or { break }.trim_right('\n')
804 trans_line := r.line.all_before('os.input(') + "'${line_t}'"
805 source_code := r.current_source_code(false, false) + '\n${trans_line}\n'
806 os.write_file(temp_file, source_code) or { panic(err) }
807 s := repl_run_vfile(temp_file) or { return 1 }
808 if s.exit_code == 0 {
809 r.add_statement_line(trans_line, trans_line)
810 }
811 } else {
812 func_call, fntype := r.function_call(r.line)
813 filter_line :=
814 r.line.replace(r.line.find_between("'", "'"), '').replace(r.line.find_between('"', '"'), '')
815 mut is_statement := false
816 if filter_line.count('=') % 2 == 1
817 && (filter_line.count('!=') + filter_line.count('>=') + filter_line.count('<=')) == 0 {
818 is_statement = true
819 } else {
820 for pattern in possible_statement_patterns {
821 if filter_line.contains(pattern) {
822 is_statement = true
823 break
824 }
825 }
826 }
827 // Note: starting a line with 2 spaces escapes the println heuristic
828 if oline.starts_with(' ') {
829 is_statement = true
830 }
831 // The parentheses do not match
832 if r.line.count('(') != r.line.count(')') {
833 is_statement = true
834 }
835
836 if !is_statement && (!func_call || fntype == FnType.fn_type) && r.line != '' {
837 print_line := 'println(${r.line})'
838 source_code := r.current_source_code(false, false) + '\n${print_line}\n'
839 os.write_file(temp_file, source_code) or { panic(err) }
840 s := repl_run_vfile(temp_file) or { return 1 }
841 if s.exit_code == 0 {
842 if s.output.len > r.last_output.len {
843 cur_line_output := s.output[r.last_output.len..]
844 print_output(cur_line_output)
845 if !cur_line_output.contains('warning:') {
846 r.last_output = s.output.clone()
847 r.add_statement_line(print_line, print_line)
848 }
849 }
850 continue
851 } else {
852 if s.output.len > r.last_output.len {
853 cur_line_output := s.output[r.last_output.len..]
854 if cur_line_output.contains('undefined ident:') {
855 print_output(cur_line_output)
856 continue
857 }
858 }
859 }
860 }
861
862 starts_with_const := starts_with_type_decl(r.line, 'const')
863 starts_with_type := starts_with_type_decl(r.line, 'type')
864 starts_with_import := r.line.starts_with('import ') || r.line.starts_with('import\t')
865 starts_with_include := r.line.starts_with('#include ')
866 || r.line.starts_with('#include\t')
867 mut temp_source_code := ''
868
869 if starts_with_import {
870 mod := r.line.fields()[1]
871 if mod !in r.modules {
872 temp_source_code = '${r.line}\n' + r.current_source_code(false, true)
873 }
874 } else if r.line.len == 0 {
875 if was_func {
876 temp_source_code = r.insert_source_code(DeclType.fn, r.temp_lines)
877 } else if was_struct {
878 temp_source_code = r.insert_source_code(DeclType.struct, r.temp_lines)
879 } else if was_enum {
880 temp_source_code = r.insert_source_code(DeclType.enum, r.temp_lines)
881 } else if was_interface {
882 temp_source_code = r.insert_source_code(DeclType.interface, r.temp_lines)
883 } else {
884 temp_source_code = r.insert_source_code(DeclType.stmt, r.temp_lines)
885 }
886 } else if starts_with_include {
887 temp_source_code = r.insert_source_code(DeclType.include, [r.line])
888 } else if starts_with_fn {
889 temp_source_code = r.insert_source_code(DeclType.fn, [r.line])
890 } else if starts_with_const {
891 temp_source_code = r.insert_source_code(DeclType.const, [r.line])
892 } else if starts_with_enum {
893 temp_source_code = r.insert_source_code(DeclType.enum, [r.line])
894 } else if starts_with_struct {
895 temp_source_code = r.insert_source_code(DeclType.struct, [r.line])
896 } else if starts_with_interface {
897 temp_source_code = r.insert_source_code(DeclType.interface, [r.line])
898 } else if starts_with_type {
899 temp_source_code = r.insert_source_code(DeclType.type, [r.line])
900 } else {
901 temp_source_code = r.current_source_code(true, false) + '\n${r.line}\n'
902 }
903 os.write_file(temp_file, temp_source_code) or { panic(err) }
904 check_only := starts_with_import || starts_with_include || starts_with_fn
905 || starts_with_const || starts_with_enum || starts_with_struct
906 || starts_with_interface || starts_with_type
907 || (r.line.len == 0 && (was_func || was_struct || was_enum || was_interface))
908 mut s := os.Result{}
909 if check_only {
910 s = repl_check_vfile(temp_file) or { return 1 }
911 } else {
912 s = repl_run_vfile(temp_file) or { return 1 }
913 }
914 if s.exit_code == 0 {
915 if starts_with_import {
916 r.parse_import(r.line)
917 } else if r.line.len == 0 {
918 if was_func {
919 r.functions << r.temp_lines
920 } else if was_struct {
921 r.structs << r.temp_lines
922 } else if was_enum {
923 r.enums << r.temp_lines
924 } else if was_interface {
925 r.interfaces << r.temp_lines
926 } else {
927 r.add_statement_lines(r.temp_lines)
928 }
929 } else if starts_with_include {
930 r.includes << r.line
931 } else if starts_with_fn {
932 r.functions << r.line
933 } else if starts_with_const {
934 r.consts << r.line
935 } else if starts_with_enum {
936 r.enums << r.line
937 } else if starts_with_type {
938 r.types << r.line
939 } else if starts_with_struct {
940 r.structs << r.line
941 } else if starts_with_interface {
942 r.interfaces << r.line
943 } else {
944 exec_line := r.snapshot_assignment_line(temp_source_code, r.line)
945 r.add_statement_line(r.line, exec_line)
946 }
947 }
948 r.temp_lines.clear()
949 if r.is_pin {
950 r.pin()
951 println('')
952 }
953 if s.output.len > r.last_output.len {
954 len := r.last_output.len
955 if s.exit_code == 0 {
956 r.last_output = s.output.clone()
957 }
958 cur_line_output := s.output[len..]
959 print_output(cur_line_output)
960 }
961 }
962 }
963 return 0
964}
965
966fn convert_output(os_result string) string {
967 lines := os_result.trim_right('\n\r').split_into_lines()
968 mut content := ''
969 for line in lines {
970 if line.contains('.vrepl_temp.v:') {
971 // Hide the temporary file name
972 sline := line.all_after('.vrepl_temp.v:')
973 idx := sline.index(' ') or {
974 content += endline_if_missed(sline)
975 return content
976 }
977 content += endline_if_missed(sline[idx + 1..])
978 } else {
979 content += endline_if_missed(line)
980 }
981 }
982 return content
983}
984
985fn print_output(os_result string) {
986 content := convert_output(os_result)
987 print(content)
988}
989
990fn main() {
991 // Support for the parameters replfolder and replprefix is needed
992 // so that the repl can be launched in parallel by several different
993 // threads by the REPL test runner.
994 args := cmdline.options_after(os.args, ['repl'])
995 replfolder := os.real_path(cmdline.option(args, '-replfolder', repl_folder))
996 replprefix := cmdline.option(args, '-replprefix', 'noprefix.${rand.ulid()}.')
997 if !os.exists(os.getenv('VEXE')) {
998 println('Usage:')
999 println(' VEXE=vexepath vrepl\n')
1000 println(' ... where vexepath is the full path to the v executable file')
1001 return
1002 }
1003 if !is_stdin_a_pipe {
1004 os.setenv('VCOLORS', 'always', true)
1005 }
1006 exit(run_repl(replfolder, replprefix))
1007}
1008
1009fn rerror(s string) {
1010 println('V repl error: ${s}')
1011 os.flush()
1012}
1013
1014fn (mut r Repl) get_one_line(prompt string) ?string {
1015 if is_stdin_a_pipe {
1016 iline := os.get_raw_line()
1017 if iline.len == 0 {
1018 return none
1019 }
1020 return iline
1021 }
1022 rline := r.readline.read_line(prompt) or { return none }
1023 return rline
1024}
1025
1026fn cleanup_files(file string) {
1027 os.rm(file) or {}
1028 $if windows {
1029 os.rm(file[..file.len - 2] + '.exe') or {}
1030 $if msvc {
1031 os.rm(file[..file.len - 2] + '.ilk') or {}
1032 os.rm(file[..file.len - 2] + '.pdb') or {}
1033 }
1034 } $else {
1035 os.rm(file[..file.len - 2]) or {}
1036 }
1037}
1038
1039fn execute_repl_v_command(v_path string, args []string) !os.Result {
1040 $if windows {
1041 mut process := os.new_process(v_path)
1042 process.set_args(args)
1043 process.set_redirect_stdio()
1044 process.create_no_window = true
1045 process.wait()
1046 stdout_output := process.stdout_slurp()
1047 stderr_output := process.stderr_slurp()
1048 exit_code := process.code
1049 process.close()
1050 return os.Result{
1051 exit_code: exit_code
1052 output: stdout_output + stderr_output
1053 }
1054 } $else {
1055 mut cmd := os.quoted_path(v_path)
1056 for arg in args {
1057 cmd += ' ' + os.quoted_path(arg)
1058 }
1059 return os.execute(cmd)
1060 }
1061}
1062
1063fn repl_run_vfile(file string) !os.Result {
1064 $if trace_repl_temp_files ? {
1065 eprintln('>> repl_run_vfile file: ${file}')
1066 }
1067 s := execute_repl_v_command(vexe, ['-message-limit', '1', '-repl', 'run', file])!
1068 if s.exit_code < 0 {
1069 rerror(s.output)
1070 return error(s.output)
1071 }
1072 return s
1073}
1074
1075fn repl_check_vfile(file string) !os.Result {
1076 $if trace_repl_temp_files ? {
1077 eprintln('>> repl_check_vfile file: ${file}')
1078 }
1079 // Declaration-only REPL lines do not need code generation or execution.
1080 // Cached checks keep repeated imports and definitions responsive.
1081 s :=
1082 execute_repl_v_command(vexe, ['-usecache', '-message-limit', '1', '-repl', '-check', file])!
1083 if s.exit_code < 0 {
1084 rerror(s.output)
1085 return error(s.output)
1086 }
1087 return s
1088}
1089