-
Notifications
You must be signed in to change notification settings - Fork 18
Expand file tree
/
Copy pathmain.v
More file actions
403 lines (368 loc) · 13.1 KB
/
main.v
File metadata and controls
403 lines (368 loc) · 13.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
// Copyright (c) 2024 Alexander Medvednikov. All rights reserved.
// Use of this source code is governed by a GPL license that can be found in the LICENSE file.
module main
import os
import flag
import term
import strings
import v.util.diff
struct App {
mut:
sb strings.Builder
is_fn_call bool // for lowercase idents
tests_ok bool = true
skip_first_arg bool // for `strings.Replace(s...)` => `s.replace(...)`
skip_call_parens bool // skip generating () and args for fully-handled calls
force_upper bool // for `field Type` in struct decl, `mod.UpperCase` types etc
no_star bool // To skip & in StarExpr in type matches (interfaces)
type_decl_name string
is_enum_decl bool
is_mut_recv bool // so that `mut f Foo` is generated instead of `mut f &Foo`
in_go_stmt bool // inside a go statement (don't convert IIFE to block)
in_defer_block bool // inside a defer block (return not allowed)
in_interface_decl bool // inside an interface declaration (skip fn prefix)
cur_fn_names map[string]bool // for fixing shadowing
name_mapping map[string]string // Go name to V name mapping for renamed variables
running_test bool // disables shadowing for now
struct_or_alias []string // skip camel_to_snake for these, but force capitalize
named_return_params map[string]bool // for named return parameters like `func foo() (im int)`
named_return_types map[string]Type // types for named return parameters
pending_named_returns bool // true when entering function body, need to declare named returns
global_names map[string]bool // track all global names (functions, structs, etc.) to avoid collisions
inline_struct_count int // counter for generating unique inline struct names
temp_var_count int // counter for generating unique temp variable names
pending_structs []string // inline struct definitions to output
enum_types map[string]bool // types that will become enums (detected by pre-scan)
enum_values map[string]bool // enum constant values (for adding . prefix in match)
array_type_aliases map[string]bool // type aliases to array types (for correct composite lit handling)
fmt_needs_closing_paren bool // for wrapping printf in print(strconv.v_sprintf(...))
at_stmt_level bool // true when we can safely emit temp var declarations
error_vars map[string]bool // variables that hold error/option types (for nil -> none translation)
first_arg_needs_mut bool // for binary.PutUint* functions where first arg needs mut
current_iota_value int // current iota value in const block (starts at 0)
in_const_block bool // true when processing a const block
last_const_expr Expr // last expression in const block (for iota pattern reuse)
call_arg_temp_vars map[string]string // temp vars for module-qualified composite lits in call args
current_call_rhs_idx int // current RHS index being processed in assignment
in_unsafe_block bool // true when generating inside unsafe {} block
imported_modules map[string]bool // track already imported modules to avoid duplicates
required_imports map[string]bool // imports required based on actual code usage (added at end)
}
fn (mut app App) genln(s string) {
app.sb.writeln(s)
}
fn (mut app App) gen(s string) {
app.sb.write_string(s)
}
// require_import marks a module as required based on actual code usage
fn (mut app App) require_import(mod string) {
app.required_imports[mod] = true
}
fn (mut app App) generate_v_code(go_file GoFile) string {
// Pre-scan to identify enum types (types used with iota in const blocks)
app.scan_for_enum_types(go_file.decls)
app.genln('module ${app.go2v_ident(go_file.package_name.name)}\n')
for decl in go_file.decls {
match decl {
FuncDecl {
app.func_decl(decl)
}
GenDecl {
app.gen_decl(decl)
}
}
}
// Output any pending inline struct definitions
for struct_def in app.pending_structs {
app.gen(struct_def)
}
mut result := app.sb.str()
// Insert required imports that were discovered during code generation
if app.required_imports.len > 0 {
mut imports_str := ''
for imp, _ in app.required_imports {
if imp !in app.imported_modules {
imports_str += 'import ${imp}\n'
}
}
if imports_str.len > 0 {
// Find the end of the module line and insert imports there
if idx := result.index('\n\n') {
result = result[..idx + 1] + imports_str + result[idx + 1..]
}
}
}
return result
}
// scan_for_enum_types pre-scans declarations to identify types that will become enums
// and their values. This allows type_decl to skip generating type aliases for these types
// and allows enum values to be prefixed with . when used in switch/match expressions
fn (mut app App) scan_for_enum_types(decls []Decls) {
for decl in decls {
if decl is GenDecl {
if decl.tok == 'const' {
// Track if we're in an enum block
mut in_enum_block := false
for spec in decl.specs {
if spec is ValueSpec {
// Check if this const starts an enum (uses iota but not bitflag)
if spec.values.len > 0 {
first_val := spec.values[0]
if app.contains_iota(first_val) && !app.is_bitflag_pattern(first_val) {
in_enum_block = true
// Record the type name
if spec.typ is Ident {
ident := spec.typ as Ident
if ident.name != '' {
app.enum_types[ident.name] = true
}
}
}
}
// If we're in an enum block, track all const names as enum values
if in_enum_block {
for name in spec.names {
v_name := app.go2v_ident(name.name)
app.enum_values[v_name] = true
}
}
}
}
}
}
}
}
fn (mut app App) typ(t Type) {
app.force_upper = true
match t {
Ident {
// Check if it's a basic type
conversion := go2v_type_checked(t.name)
if conversion.is_basic {
// It's a basic type, use the converted value directly
app.gen(conversion.v_type)
} else {
// It's a custom type, use go2v_ident for struct/alias handling
app.gen(app.go2v_ident(t.name))
}
}
ArrayType {
app.array_type(t)
}
ChanType {
app.chan_type(t)
}
Ellipsis {
// Variadic parameter: ...T in Go => ...T in V
app.gen('...')
conversion := go2v_type_checked(t.elt.name)
if conversion.is_basic {
app.gen(conversion.v_type)
} else {
app.gen(app.go2v_ident(t.elt.name))
}
}
MapType {
app.map_type(t)
}
StarExpr {
// Skip & if receiver is mut
if app.is_mut_recv {
app.expr(t.x)
app.is_mut_recv = false
} else {
// V arrays are already references, so *[]T becomes just []T
if t.x is ArrayType {
app.array_type(t.x)
} else {
app.star_expr(t)
}
}
}
SelectorExpr {
app.selector_expr(t)
}
StructType {
app.struct_type(t)
}
InvalidExpr {
app.gen('INVALID_EXPR')
}
InterfaceType {
app.interface_type(t)
}
FuncType {
app.func_type(t)
}
}
app.force_upper = false
}
fn generate_ast_for_go_file(go_file_path string) string {
if !os.exists(go_file_path) {
eprintln('Missing input .go file: `${go_file_path}`.')
return ''
}
output_file := go_file_path + '.json'
asty_cmd := '${full_path_to_asty} go2json -imports -comments -indent 2 -input ${go_file_path} -output ${output_file}'
println('generating ast for ${go_file_path} with\n${asty_cmd}')
run_result := os.system(asty_cmd)
if run_result != 0 {
eprintln('Failed to run asty. Please install it with: `go install github.com/asty-org/asty@latest`.')
return ''
}
json_content := os.read_file(output_file) or {
eprintln('Failed to read ${output_file}')
return ''
}
// Replace "NodeType": " with "_type": " to handle sum types
updated_content := json_content.replace('"NodeType": "', '"_type": "')
os.write_file(output_file, updated_content) or {
eprintln('Failed to write to ${output_file}')
return ''
}
return output_file
}
fn (mut app App) translate_file(go_file_path string) {
println('Translating a single Go file ${go_file_path}...')
ast_file := generate_ast_for_go_file(go_file_path)
go_file := parse_go_ast(ast_file) or {
eprintln('Failed to parse Go AST 1: ${err}')
return
}
generated_v_code := app.generate_v_code(go_file)
v_path := go_file_path.substr_ni(0, -3) + '.v'
os.write_file(v_path, generated_v_code) or { panic(err) }
println('${v_path} has been successfully generated')
// Note: Skipping vfmt to preserve space before { in module-qualified struct literals
// This works around a V parser issue where api.Type{ inside function args fails
// but api.Type { (with space) works. Unfortunately vfmt removes the space.
// res := os.system('v -translated-go fmt -w ${v_path}')
// if res != 0 {
// exit(1)
// }
}
fn (mut app App) run_test(subdir string, test_name string) ! {
app.running_test = true
go_file_path := '${subdir}/${test_name}/${test_name}.go.json'
is_complex_test := subdir.starts_with('complex_tests')
go_file := parse_go_ast(go_file_path) or {
eprintln('Failed to parse Go AST 2: ${err}')
return
}
tmpdir := os.temp_dir()
generated_v_code := app.generate_v_code(go_file)
v_path := '${tmpdir}/${test_name}.v'
os.write_file(v_path, generated_v_code) or { panic(err) }
res := os.execute('v -translated-go fmt -w ${v_path}')
if res.exit_code != 0 {
println(res)
app.tests_ok = false
}
println('Running test ${test_name}...')
// For complex tests, just verify that translation and vfmt succeeded
if is_complex_test {
if res.exit_code == 0 {
println(term.green('OK (translation + vfmt)'))
} else {
println('Test ${test_name} failed: vfmt returned non-zero exit code')
}
return
}
// For simple tests, compare against .vv file
expected_v_code_path := '${subdir}/${test_name}/${test_name}.vv'
mut formatted_v_code := os.read_file(v_path) or { panic(err) }
formatted_v_code = formatted_v_code.replace('\n\n\tprint', '\n\tprint') // TODO
// println('formatted:')
// println(formatted_v_code)
expected_v_code := os.read_file(expected_v_code_path) or {
eprintln('Failed to read expected V code: ${err}')
return
}
// if generated_v_code == expected_v_code {
// if trim_space(formatted_v_code) == trim_space(expected_v_code) {
if formatted_v_code == expected_v_code {
println(term.green('OK'))
} else {
println('Test ${test_name} failed.')
app.tests_ok = false
println('Generated V code:')
println(generated_v_code)
if res.exit_code == 0 {
println('=======================\nFormatted V code:')
println(formatted_v_code)
}
println('=======================\nExpected V code:')
println(expected_v_code)
if res.exit_code == 0 {
print_diff_line(formatted_v_code, expected_v_code)
if diff_ := diff.compare_text(expected_v_code, formatted_v_code) {
println(diff_)
}
}
println('=======================\nGo code:')
go_code := os.read_file(expected_v_code_path.replace('.vv', '.go')) or { panic(err) }
println(go_code)
}
}
fn print_diff_line(formatted_v_code string, expected_v_code string) {
lines0 := formatted_v_code.split('\n')
lines1 := expected_v_code.split('\n')
for i in 0 .. lines0.len {
if i >= lines1.len {
return
}
if lines0[i].trim_space() != lines1[i].trim_space() {
println('!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!')
println(lines0[i])
println('^^^^^^^^^^^^^^^^^^^^^^^^^^^^^')
return
}
}
}
fn create_json(subdir string, test_name string) {
input_file := '${subdir}/${test_name}/${test_name}.go'
// output_file := '${input_file}.json'
generate_ast_for_go_file(input_file)
}
fn main() {
mut fp := flag.new_flag_parser(os.args)
fp.application('go2v')
fp.version('0.0.1')
fp.description('go2v is an utility to automatically transpile Go source code into V source code')
fp.usage_example('file.go')
fp.usage_example('tests/import_strings')
fp.footer('\nNormal mode:')
fp.footer(' Supply the path to a single .go file, that you want translated to V.')
fp.footer('\nTest folder mode:')
fp.footer(' The test folder is expected to have 2 files in it - a .go file and a .vv file.')
fp.footer(' The .go file will be translated, and then the output will be compared to the .vv file.')
fp.footer(' A failure will be reported, if there are differences.')
fp.limit_free_args_to_exactly(1)!
fp.skip_executable()
mut go_file_name := fp.finalize() or {
eprintln(err)
println(fp.usage())
exit(1)
}[0]
ensure_asty_is_installed() or {
eprintln(err)
exit(1)
}
mut app := &App{
sb: strings.new_builder(1000)
}
if go_file_name.ends_with('.go') {
if !os.exists(go_file_name) {
eprintln('go2v error: missing file `${go_file_name}`')
exit(1)
}
app.translate_file(go_file_name)
return
}
mut subdir := 'tests'
go_file_name = go_file_name.trim_right('/')
subdir = os.dir(go_file_name)
test_name := os.base(go_file_name)
create_json(subdir, test_name)
app.run_test(subdir, test_name)!
}