Add a file tag parser to core:odin/parser

This commit is contained in:
Damian Tarnawski
2024-08-12 15:21:46 +02:00
parent 94a1a7aed5
commit 92821300e4
2 changed files with 389 additions and 0 deletions
+256
View File
@@ -0,0 +1,256 @@
package odin_parser
import "base:runtime"
import "core:strings"
import "../ast"
Private_Flag :: enum {
Public,
Package,
File,
}
Odin_OS_Type :: runtime.Odin_OS_Type
Odin_Arch_Type :: runtime.Odin_Arch_Type
Odin_OS_Types :: bit_set[Odin_OS_Type]
Odin_Arch_Types :: bit_set[Odin_Arch_Type]
Build_Kind :: struct {
os: Odin_OS_Types,
arch: Odin_Arch_Types,
}
File_Tags :: struct {
build_project_name: []string,
build: []Build_Kind,
private: Private_Flag,
ignore: bool,
lazy: bool,
no_instrumentation: bool,
}
ALL_ODIN_OS_TYPES :: Odin_OS_Types{
.Windows,
.Darwin,
.Linux,
.Essence,
.FreeBSD,
.OpenBSD,
.NetBSD,
.Haiku,
.WASI,
.JS,
.Orca,
.Freestanding,
}
ALL_ODIN_ARCH_TYPES :: Odin_Arch_Types{
.amd64,
.i386,
.arm32,
.arm64,
.wasm32,
.wasm64p32,
}
ODIN_OS_NAMES :: [Odin_OS_Type]string{
.Unknown = "",
.Windows = "windows",
.Darwin = "darwin",
.Linux = "linux",
.Essence = "essence",
.FreeBSD = "freebsd",
.OpenBSD = "openbsd",
.NetBSD = "netbsd",
.Haiku = "haiku",
.WASI = "wasi",
.JS = "js",
.Orca = "orca",
.Freestanding = "freestanding",
}
ODIN_ARCH_NAMES :: [Odin_Arch_Type]string{
.Unknown = "",
.amd64 = "amd64",
.i386 = "i386",
.arm32 = "arm32",
.arm64 = "arm64",
.wasm32 = "wasm32",
.wasm64p32 = "wasm64p32",
}
@require_results
get_build_os_from_string :: proc(str: string) -> Odin_OS_Type {
for os_name, os in ODIN_OS_NAMES {
if strings.equal_fold(os_name, str) {
return os
}
}
return .Unknown
}
@require_results
get_build_arch_from_string :: proc(str: string) -> Odin_Arch_Type {
for arch_name, arch in ODIN_ARCH_NAMES {
if strings.equal_fold(arch_name, str) {
return arch
}
}
return .Unknown
}
parse_file_tags :: proc(file: ast.File) -> (tags: File_Tags) {
if file.docs == nil {
return
}
next_char :: proc(src: string, i: ^int) -> (ch: u8) {
if i^ < len(src) {
ch = src[i^]
}
i^ += 1
return
}
skip_whitespace :: proc(src: string, i: ^int) {
for {
switch next_char(src, i) {
case ' ', '\t':
continue
case:
i^ -= 1
return
}
}
}
skip_rest_of_line :: proc(src: string, i: ^int) {
for {
switch next_char(src, i) {
case '\n', 0:
return
case:
continue
}
}
}
scan_value :: proc(src: string, i: ^int) -> string {
start := i^
for {
switch next_char(src, i) {
case ' ', '\t', '\n', '\r', 0, ',':
i^ -= 1
return src[start:i^]
case:
continue
}
}
}
build_kinds: [dynamic]Build_Kind
defer shrink(&build_kinds)
build_project_names: [dynamic]string
defer shrink(&build_project_names)
for comment in file.docs.list {
if len(comment.text) < 3 || comment.text[:2] != "//" {
continue
}
text := comment.text[2:]
i := 0
skip_whitespace(text, &i)
if next_char(text, &i) == '+' {
switch scan_value(text, &i) {
case "ignore":
tags.ignore = true
case "lazy":
tags.lazy = true
case "no-instrumentation":
tags.no_instrumentation = true
case "private":
skip_whitespace(text, &i)
switch scan_value(text, &i) {
case "file":
tags.private = .File
case "package", "":
tags.private = .Package
}
case "build-project-name":
values_loop: for {
skip_whitespace(text, &i)
name_start := i
switch next_char(text, &i) {
case 0, '\n':
i -= 1
break values_loop
case '!':
// include ! in the name
case:
i -= 1
}
scan_value(text, &i)
append(&build_project_names, text[name_start:i])
}
case "build":
kinds_loop: for {
os_positive: Odin_OS_Types
os_negative: Odin_OS_Types
arch_positive: Odin_Arch_Types
arch_negative: Odin_Arch_Types
defer append(&build_kinds, Build_Kind{
os = (os_positive == {} ? ALL_ODIN_OS_TYPES : os_positive) -os_negative,
arch = (arch_positive == {} ? ALL_ODIN_ARCH_TYPES : arch_positive)-arch_negative,
})
for {
skip_whitespace(text, &i)
is_notted: bool
switch next_char(text, &i) {
case 0, '\n':
i -= 1
break kinds_loop
case ',':
continue kinds_loop
case '!':
is_notted = true
case:
i -= 1
}
value := scan_value(text, &i)
if value == "ignore" {
tags.ignore = true
} else if os := get_build_os_from_string(value); os != .Unknown {
if is_notted {
os_negative += {os}
} else {
os_positive += {os}
}
} else if arch := get_build_arch_from_string(value); arch != .Unknown {
if is_notted {
arch_negative += {arch}
} else {
arch_positive += {arch}
}
}
}
}
}
}
skip_rest_of_line(text, &i)
}
tags.build = build_kinds[:]
tags.build_project_name = build_project_names[:]
return
}
+133
View File
@@ -0,0 +1,133 @@
package test_core_odin_parser
import "base:runtime"
import "core:testing"
import "core:slice"
import "core:odin/ast"
import "core:odin/parser"
@test
test_parse_file_tags :: proc(t: ^testing.T) {
context.allocator = context.temp_allocator
runtime.DEFAULT_TEMP_ALLOCATOR_TEMP_GUARD()
Test_Case :: struct {
src: string,
tags: parser.File_Tags,
}
test_cases := []Test_Case{
{// [0]
src = ``,
tags = {},
}, {// [1]
src = `
package main
`,
tags = {},
}, {// [2]
src = `
//+build linux, darwin, freebsd, openbsd, netbsd, haiku
//+build arm32, arm64
package main
`,
tags = {
build = {
{os = {.Linux}, arch = parser.ALL_ODIN_ARCH_TYPES},
{os = {.Darwin}, arch = parser.ALL_ODIN_ARCH_TYPES},
{os = {.FreeBSD}, arch = parser.ALL_ODIN_ARCH_TYPES},
{os = {.OpenBSD}, arch = parser.ALL_ODIN_ARCH_TYPES},
{os = {.NetBSD}, arch = parser.ALL_ODIN_ARCH_TYPES},
{os = {.Haiku}, arch = parser.ALL_ODIN_ARCH_TYPES},
{os = parser.ALL_ODIN_OS_TYPES, arch = {.arm32}},
{os = parser.ALL_ODIN_OS_TYPES, arch = {.arm64}},
},
},
}, {// [3]
src = `
// +private
//+lazy
// +no-instrumentation
//+ignore
// some other comment
package main
`,
tags = {
private = .Package,
no_instrumentation = true,
lazy = true,
ignore = true,
},
}, {// [4]
src = `
//+build-project-name foo !bar
//+build js wasm32, js wasm64p32
package main
`,
tags = {
build_project_name = {"foo", "!bar"},
build = {
{
os = {.JS},
arch = {.wasm32},
}, {
os = {.JS},
arch = {.wasm64p32},
},
},
},
},
}
expect :: proc(t: ^testing.T, ok: bool, name: string, i: int, expected, actual: $T, loc := #caller_location) {
testing.expectf(t, ok,
"[%d] expected %s:\n\e[0;32m%#v\e[0m, actual:\n\e[0;31m%#v\e[0m",
i, name, expected, actual, loc=loc
)
}
for test_case, i in test_cases {
file := ast.File{
fullpath = "test.odin",
src = test_case.src,
}
p := parser.default_parser()
ok := parser.parse_file(&p, &file)
testing.expect(t, ok, "bad parse")
tags := parser.parse_file_tags(file)
expect(t,
slice.equal(test_case.tags.build_project_name, tags.build_project_name),
"build_project_name", i, test_case.tags.build_project_name, tags.build_project_name,
)
expect(t,
slice.equal(test_case.tags.build, tags.build),
"build", i, test_case.tags.build, tags.build,
)
expect(t,
test_case.tags.private == tags.private,
"private", i, test_case.tags.private, tags.private,
)
expect(t,
test_case.tags.ignore == tags.ignore,
"ignore", i, test_case.tags.ignore, tags.ignore,
)
expect(t,
test_case.tags.lazy == tags.lazy,
"lazy", i, test_case.tags.lazy, tags.lazy,
)
expect(t,
test_case.tags.no_instrumentation == tags.no_instrumentation,
"no_instrumentation", i, test_case.tags.no_instrumentation, tags.no_instrumentation,
)
}
}