Browse Source

Merge pull request #4009 from thetarnav/file-tag-parser

Add a file tag parser to core:odin/parser
gingerBill 11 months ago
parent
commit
773703bc83
3 changed files with 423 additions and 0 deletions
  1. 29 0
      base/runtime/core.odin
  2. 239 0
      core/odin/parser/file_tags.odin
  3. 155 0
      tests/core/odin/test_file_tags.odin

+ 29 - 0
base/runtime/core.odin

@@ -546,10 +546,23 @@ Odin_OS_Type :: type_of(ODIN_OS)
 		arm64,
 		wasm32,
 		wasm64p32,
+		riscv64,
 	}
 */
 Odin_Arch_Type :: type_of(ODIN_ARCH)
 
+Odin_Arch_Types :: bit_set[Odin_Arch_Type]
+
+ALL_ODIN_ARCH_TYPES :: Odin_Arch_Types{
+	.amd64,
+	.i386,
+	.arm32,
+	.arm64,
+	.wasm32,
+	.wasm64p32,
+	.riscv64,
+}
+
 /*
 	// Defined internally by the compiler
 	Odin_Build_Mode_Type :: enum int {
@@ -573,6 +586,22 @@ Odin_Build_Mode_Type :: type_of(ODIN_BUILD_MODE)
 */
 Odin_Endian_Type :: type_of(ODIN_ENDIAN)
 
+Odin_OS_Types :: bit_set[Odin_OS_Type]
+
+ALL_ODIN_OS_TYPES :: Odin_OS_Types{
+	.Windows,
+	.Darwin,
+	.Linux,
+	.Essence,
+	.FreeBSD,
+	.OpenBSD,
+	.NetBSD,
+	.Haiku,
+	.WASI,
+	.JS,
+	.Orca,
+	.Freestanding,
+}
 
 /*
 	// Defined internally by the compiler

+ 239 - 0
core/odin/parser/file_tags.odin

@@ -0,0 +1,239 @@
+package odin_parser
+
+import "base:runtime"
+import "core:strings"
+import "core:reflect"
+
+import "../ast"
+
+Private_Flag :: enum {
+	Public,
+	Package,
+	File,
+}
+
+Build_Kind :: struct {
+	os:   runtime.Odin_OS_Types,
+	arch: runtime.Odin_Arch_Types,
+}
+
+File_Tags :: struct {
+	build_project_name: [][]string,
+	build:              []Build_Kind,
+	private:            Private_Flag,
+	ignore:             bool,
+	lazy:               bool,
+	no_instrumentation: bool,
+}
+
+@require_results
+get_build_os_from_string :: proc(str: string) -> runtime.Odin_OS_Type {
+	fields := reflect.enum_fields_zipped(runtime.Odin_OS_Type)
+	for os in fields {
+		if strings.equal_fold(os.name, str) {
+			return runtime.Odin_OS_Type(os.value)
+		}
+	}
+	return .Unknown
+}
+@require_results
+get_build_arch_from_string :: proc(str: string) -> runtime.Odin_Arch_Type {
+	fields := reflect.enum_fields_zipped(runtime.Odin_Arch_Type)
+	for os in fields {
+		if strings.equal_fold(os.name, str) {
+			return runtime.Odin_Arch_Type(os.value)
+		}
+	}
+	return .Unknown
+}
+
+@require_results
+parse_file_tags :: proc(file: ast.File, allocator := context.allocator) -> (tags: File_Tags) {
+	context.allocator = allocator
+
+	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
+			}
+		}
+	}
+	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_name_strings: [dynamic]string
+	defer shrink(&build_project_name_strings)
+
+	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":
+				groups_loop: for {
+					index_start := len(build_project_name_strings)
+
+					defer append(&build_project_names, build_project_name_strings[index_start:])
+
+					for {
+						skip_whitespace(text, &i)
+						name_start := i
+	
+						switch next_char(text, &i) {
+						case 0, '\n':
+							i -= 1
+							break groups_loop
+						case ',':
+							continue groups_loop
+						case '!':
+							// include ! in the name
+						case:
+							i -= 1
+						}
+	
+						scan_value(text, &i)
+						append(&build_project_name_strings, text[name_start:i])
+					}
+
+					append(&build_project_names, build_project_name_strings[index_start:])
+				}
+			case "build":
+				kinds_loop: for {
+					os_positive: runtime.Odin_OS_Types
+					os_negative: runtime.Odin_OS_Types
+
+					arch_positive: runtime.Odin_Arch_Types
+					arch_negative: runtime.Odin_Arch_Types
+
+					defer append(&build_kinds, Build_Kind{
+						os   = (os_positive   == {} ? runtime.ALL_ODIN_OS_TYPES   : os_positive)  -os_negative,
+						arch = (arch_positive == {} ? runtime.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}
+							}
+						}
+					}
+				}
+			}
+		}
+	}
+
+	tags.build = build_kinds[:]
+	tags.build_project_name = build_project_names[:]
+
+	return
+}
+
+Build_Target :: struct {
+	os:           runtime.Odin_OS_Type,
+	arch:         runtime.Odin_Arch_Type,
+	project_name: string,
+}
+
+@require_results
+match_build_tags :: proc(file_tags: File_Tags, target: Build_Target) -> bool {
+
+	project_name_correct := len(target.project_name) == 0 || len(file_tags.build_project_name) == 0
+
+	for group in file_tags.build_project_name {
+		group_correct := true
+		for name in group {
+			if name[0] == '!' {
+				group_correct &&= target.project_name != name[1:]
+			} else {
+				group_correct &&= target.project_name == name
+			}
+		}
+		project_name_correct ||= group_correct
+	}
+
+	os_and_arch_correct := len(file_tags.build) == 0
+
+	for kind in file_tags.build {
+		os_and_arch_correct ||= target.os in kind.os && target.arch in kind.arch
+	}
+
+	return !file_tags.ignore && project_name_correct && os_and_arch_correct
+}

+ 155 - 0
tests/core/odin/test_file_tags.odin

@@ -0,0 +1,155 @@
+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,
+		matching_targets: []struct{
+			target: parser.Build_Target,
+			result: bool,
+		},
+	}
+
+	test_cases := []Test_Case{
+		{// [0]
+			src = ``,
+			tags = {},
+		}, {// [1]
+			src = `
+package main
+			`,
+			tags = {},
+			matching_targets = {
+				{{.Windows, .amd64, "foo"}, true},
+			},
+		}, {// [2]
+			src = `
+//+build linux, darwin, freebsd, openbsd, netbsd, haiku
+//+build arm32, arm64
+package main
+			`,
+			tags = {
+				build = {
+					{os = {.Linux},   arch = runtime.ALL_ODIN_ARCH_TYPES},
+					{os = {.Darwin},  arch = runtime.ALL_ODIN_ARCH_TYPES},
+					{os = {.FreeBSD}, arch = runtime.ALL_ODIN_ARCH_TYPES},
+					{os = {.OpenBSD}, arch = runtime.ALL_ODIN_ARCH_TYPES},
+					{os = {.NetBSD},  arch = runtime.ALL_ODIN_ARCH_TYPES},
+					{os = {.Haiku},   arch = runtime.ALL_ODIN_ARCH_TYPES},
+					{os = runtime.ALL_ODIN_OS_TYPES, arch = {.arm32}},
+					{os = runtime.ALL_ODIN_OS_TYPES, arch = {.arm64}},
+				},
+			},
+			matching_targets = {
+				{{.Linux, .amd64, "foo"}, true},
+				{{.Windows, .arm64, "foo"}, true},
+				{{.Windows, .amd64, "foo"}, false},
+			},
+		}, {// [3]
+			src = `
+// +private
+//+lazy
+//	+no-instrumentation
+//+ignore
+// some other comment
+package main
+			`,
+			tags = {
+				private            = .Package,
+				no_instrumentation = true,
+				lazy               = true,
+				ignore             = true,
+			},
+			matching_targets = {
+				{{.Linux, .amd64, "foo"}, false},
+			},
+		}, {// [4]
+			src = `
+//+build-project-name foo !bar, baz
+//+build js wasm32, js wasm64p32
+package main
+			`,
+			tags = {
+				build_project_name = {{"foo", "!bar"}, {"baz"}},
+				build = {
+					{
+						os = {.JS},
+						arch = {.wasm32},
+					}, {
+						os = {.JS},
+						arch = {.wasm64p32},
+					},
+				},
+			},
+			matching_targets = {
+				{{.JS, .wasm32, "foo"}, true},
+				{{.JS, .wasm64p32, "baz"}, true},
+				{{.JS, .wasm64p32, "bar"}, false},
+			},
+		},
+	}
+
+	for test_case, 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)
+
+
+		build_project_name_the_same: bool
+		check: if len(test_case.tags.build_project_name) == len(tags.build_project_name) {
+			for tag, i in test_case.tags.build_project_name {
+				slice.equal(tag, tags.build_project_name[i]) or_break check
+			}
+			build_project_name_the_same = true
+		}
+		testing.expectf(t, build_project_name_the_same,
+			"[%d] file_tags.build_project_name expected:\n%#v, got:\n%#v",
+			test_case_i, test_case.tags.build_project_name, tags.build_project_name)
+
+		testing.expectf(t, slice.equal(test_case.tags.build, tags.build),
+			"[%d] file_tags.build expected:\n%#v, got:\n%#v",
+			test_case_i, test_case.tags.build, tags.build)
+
+		testing.expectf(t, test_case.tags.private == tags.private,
+			"[%d] file_tags.private expected:\n%v, got:\n%v",
+			test_case_i, test_case.tags.private, tags.private)
+
+		testing.expectf(t, test_case.tags.ignore == tags.ignore,
+			"[%d] file_tags.ignore expected:\n%v, got:\n%v",
+			test_case_i, test_case.tags.ignore, tags.ignore)
+
+		testing.expectf(t, test_case.tags.lazy == tags.lazy,
+			"[%d] file_tags.lazy expected:\n%v, got:\n%v",
+			test_case_i, test_case.tags.lazy, tags.lazy)
+
+		testing.expectf(t, test_case.tags.no_instrumentation == tags.no_instrumentation,
+			"[%d] file_tags.no_instrumentation expected:\n%v, got:\n%v",
+			test_case_i, test_case.tags.no_instrumentation, tags.no_instrumentation)
+
+		for target in test_case.matching_targets {
+			matches := parser.match_build_tags(test_case.tags, target.target)
+			testing.expectf(t, matches == target.result,
+				"[%d] Expected parser.match_build_tags(%#v) == %v, got %v",
+				test_case_i, target.target, target.result, matches)
+		}
+	}
+}