diff --git a/.gitignore b/.gitignore index 3bec7105a3..28bb3ffc4e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ # Python __pycache__/ venv/ +.lark_cache* # Misc .idea/ diff --git a/.vscode/c_cpp_properties.json b/.vscode/c_cpp_properties.json index 7c226a0f4e..bf368fcfc9 100644 --- a/.vscode/c_cpp_properties.json +++ b/.vscode/c_cpp_properties.json @@ -8,7 +8,8 @@ ], "defines": [ "F3DEX_GBI_2", - "_LANGUAGE_C" + "_LANGUAGE_C", + "SCRIPT(...)={}" ], "cStandard": "c89", "cppStandard": "c++17", diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 47f7416c04..a40435c7fa 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -3,6 +3,7 @@ "ms-vscode.cpptools", "nanaian.vscode-star-rod", "notskm.clang-tidy", + "EditorConfig.EditorConfig", ], "unwantedRecommendations": [ "llvm-vs-code-extensions.vscode-clangd", diff --git a/.vscode/settings.json b/.vscode/settings.json index a9f885c65d..4b950fa383 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,6 +1,4 @@ { - "editor.detectIndentation": false, - "editor.insertSpaces": true, "files.eol": "\n", "files.insertFinalNewline": true, "editor.rulers": [120], @@ -10,6 +8,7 @@ "-Iinclude/PR", "-Isrc", "-D_LANGUAGE_C", + "-DSCRIPT(...)={}", ], "python.pythonPath": "/usr/bin/python3", } diff --git a/Doxyfile b/Doxyfile index 343634104a..48bedc8645 100644 --- a/Doxyfile +++ b/Doxyfile @@ -103,7 +103,7 @@ GENERATE_LATEX = NO ENABLE_PREPROCESSING = YES MACRO_EXPANSION = YES EXPAND_ONLY_PREDEF = YES -PREDEFINED = DOXYGEN NON_MATCHING __attribute__((x))= +PREDEFINED = DOXYGEN NON_MATCHING SCRIPT(...)={} __attribute__((x))= EXPAND_AS_DEFINED = INCLUDE_ASM UNK_TYPE UNK_PTR UNK_RET UNK_FUN_ARG UNK_FUN_PTR UNK_ARGS M SKIP_FUNCTION_MACROS = YES diff --git a/Makefile b/Makefile index 5bbe69d645..d0e7343ee3 100644 --- a/Makefile +++ b/Makefile @@ -97,7 +97,7 @@ $(BUILD_DIR)/%.s.o: %.s $(AS) $(ASFLAGS) -o $@ $< $(BUILD_DIR)/%.c.o: %.c $(H_FILES) - cpp $(CPPFLAGS) $< | $(CC) $(CFLAGS) -o - | $(OLD_AS) $(OLDASFLAGS) - -o $@ + cpp $(CPPFLAGS) $< | tools/compile_dsl_macros.py | $(CC) $(CFLAGS) -o - | $(OLD_AS) $(OLDASFLAGS) - -o $@ $(BUILD_DIR)/%.bin.o: %.bin $(LD) -r -b binary -o $@ $< diff --git a/format.sh b/format.sh index 8c3f0a677f..7cb8d8430e 100755 --- a/format.sh +++ b/format.sh @@ -1,6 +1,6 @@ #!/bin/bash -COMPILER_OPTS="-std=gnu89 -Iinclude -Isrc -D_LANGUAGE_C" +COMPILER_OPTS="-std=gnu89 -Iinclude -Isrc -D_LANGUAGE_C -DSCRIPT(...)={}" shopt -s globstar diff --git a/requirements.txt b/requirements.txt index 06b47694be..361a2122c4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ capstone PyYAML +lark-parser diff --git a/src/world/area_kmr/kmr_12/events.c b/src/world/area_kmr/kmr_12/events.c index b56c1e1d21..3a0c92088a 100644 --- a/src/world/area_kmr/kmr_12/events.c +++ b/src/world/area_kmr/kmr_12/events.c @@ -205,30 +205,22 @@ NpcGroupList M(npcGroupList) = { NPC_GROUP_LIST_END(), }; -// *INDENT-OFF* -Script M(ReadEastSign) = { - SI_CALL(func_800441F0, SI_VAR(0)), - SI_IF_EQ(SI_VAR(0), 1), - SI_RETURN(), - SI_END_IF(), +Script M(ReadEastSign) = SCRIPT({ + func_800441F0(SI_VAR(0)) + if SI_VAR(0) == 1 { + return + } - SI_GROUP(0), + setgroup 0 - SI_CALL(func_802D5830, 1), - SI_CALL(DisablePlayerInput, 1), - SI_CALL(ShowMessageAtScreenPos, MessageID_SIGN_GOOMBA_KINGS_FORTRESS_AHEAD, 160, 40), - SI_CALL(DisablePlayerInput, 0), - SI_CALL(func_802D5830, 0), + func_802D5830(1) + DisablePlayerInput(1) + ShowMessageAtScreenPos(MessageID_SIGN_GOOMBA_KINGS_FORTRESS_AHEAD, 160, 40) + DisablePlayerInput(0) + func_802D5830(0) +}); - SI_RETURN(), - SI_END(), -}; - -Script M(MakeEntities) = { - SI_CALL(MakeEntity, 0x802EAFDC, 436, 0, -42, 0, 0x80000000), - SI_CALL(AssignScript, &M(ReadEastSign)), - - SI_RETURN(), - SI_END(), -}; -// *INDENT-ON* +Script M(MakeEntities) = SCRIPT({ + MakeEntity(0x802EAFDC, 436, 0, -42, 0, 0x80000000) + AssignScript(M(ReadEastSign)) +}); diff --git a/tools/compile_dsl_macros.py b/tools/compile_dsl_macros.py new file mode 100755 index 0000000000..73f981a1d4 --- /dev/null +++ b/tools/compile_dsl_macros.py @@ -0,0 +1,229 @@ +#! /usr/bin/python3 + +from sys import stdin, stderr +from lark import Lark, exceptions, Transformer, v_args + +def eprint(*args, **kwargs): + print(*args, file=stderr, **kwargs) + +""" +write_buf = "" +def write(s): + global write_buf + write_buf += s +def flush(): + global write_buf + print(write_buf, end="") + write_buf = "" +""" +def write(s): + print(s, end="") +def flush(): + pass +#""" + +script_parser = Lark(r""" + + block: "{" line* "}" + + ?line: call + | if_stmt + | "return" -> return_stmt + | "setgroup" expr -> setgroup + + call: CNAME "(" [expr ("," expr)* [","]] ")" + + if_stmt: "if" expr if_op expr block ["else" block] + + ?if_op: "==" -> if_op_eq + + ?expr: c_const_expr + | ESCAPED_STRING + | SIGNED_INT + | DECIMAL + | HEX_INT + | CNAME + + c_const_expr: "(" (c_const_expr | NOT_PARENS)+ ")" + NOT_PARENS: /[^()]+/ + + %import common.CNAME + %import common.SIGNED_INT + %import common.DECIMAL + %import common.HEXDIGIT + %import common.ESCAPED_STRING + + HEX_INT: ["+"|"-"] "0x" HEXDIGIT+ + + LINE_COMMENT: "//" /[^\n]*/ NEWLINE + %ignore LINE_COMMENT + + %import common.WS + %import common.NEWLINE + %ignore WS + +""", start="block")#, parser="lalr", cache=True) + +def si_cmd(opcode, *args): + return [opcode, len(args), *args] + +@v_args(inline=True) +class Compile(Transformer): + SIGNED_INT = str + DECIMAL = float # TODO: fixed + HEX_INT = str + ESCAPED_STRING = eval + + def CNAME(self, name): + return f"(Bytecode)({name})" + + NOT_PARENS = str + def c_const_expr(self, *args): # usually a macro expansion + return f"(Bytecode)({' '.join(args)})" + + def block(self, *lines): + flat = [] + for line in lines: + if type(line) != list: + eprint(f"uncompiled: {line}") + else: + flat += line + return flat + + def call(self, func, *args): + # TODO: type checking etc + return si_cmd(0x43, func, *args) + + def if_stmt(self, a, op, b, block): + return si_cmd(op, a, b) + block + si_cmd(0x13) + def if_op_eq(self): return 0x0A + + def return_stmt(self): return si_cmd(0x02) + + def setgroup(self, group): + return si_cmd(0x4D, group) + +def compile_script(s): + tree = script_parser.parse(s) + #eprint(tree.pretty()) + return Compile().transform(tree) + si_cmd(0x02) + si_cmd(0x01) + +def read_until_closing_paren(depth=1, lex_strings=False): + text = "" + + in_string = False + string_escape = False + + while True: + char = stdin.read(1) + + if len(char) == 0: + # EOF + return text + + if string_escape == True: + string_escape = False + elif char == "(" and not in_string: + depth += 1 + elif char == ")" and not in_string: + depth -= 1 + if depth == 0: + break + elif char == '"' and lex_strings: + in_string = not in_string + elif char == "\\" and in_string: + string_escape = True + + text += char + + return text + +def read_line(): + line = "" + + while True: + char = stdin.read(1) + + if len(char) == 0: + # EOF + return line + + if char == "\n": + break + + line += char + + return line + +# Expects output from C preprocessor on stdin +if __name__ == "__main__": + line_no = 1 + char_no = 1 + file_info = [] + error = False + + macro_name = "" # captures recent UPPER_CASE identifier + prev_char = "" + while True: + char = stdin.read(1) + + if len(char) == 0: + # EOF + write(macro_name) + if error: + exit(1) + else: + #with open("debug.i", "w") as i: + # i.write(write_buf) + flush() + exit(0) + + if char == "#" and (prev_char == "\n" or prev_char == ""): + # cpp line/file marker + line = read_line() + line_split = line[1:].split(" ") + + line_no = int(line_split[0]) + file_info = line_split[1:] + + write("#" + line + "\n") + elif char == "(": + # SCRIPT(...) + if macro_name == "SCRIPT": + script_source = read_until_closing_paren(lex_strings=True) + + try: + bytecode = compile_script(script_source) + + write("{") + for word in bytecode: + write(f"{word},") + write("}") + except exceptions.UnexpectedToken as e: + line = e.line + line_no + filename = file_info[0][1:-1] + + eprint(e.get_context(script_source)) + eprint(f"{filename}:{line}: script parse error: unexpected `{e.token}'") + + error = True + + line_no += script_source.count("\n") + write(f"\n# {line_no} {file_info[0]}\n") + else: + # leave non-macro in source + write(macro_name + char) + + macro_name = "" + elif char == "_" or (char >= 'A' and char <= 'Z'): + macro_name += char + else: + write(macro_name + char) + macro_name = "" + + if char == "\n": + char_no = 0 + line_no += 1 + + char_no += 1 + prev_char = char diff --git a/tools/splat.yaml b/tools/splat.yaml index 9bf1d51de8..8e62f4b8b1 100644 --- a/tools/splat.yaml +++ b/tools/splat.yaml @@ -2,9 +2,9 @@ name: "Paper Mario (North America)" basename: "papermario" options: find-file-boundaries: True - pycparser_flags: ["-Iinclude", "-D_LANGUAGE_C", "-ffreestanding", "-DF3DEX_GBI_2", "-DSPLAT"] compiler: "GCC" mnemonic_ljust: 10 + o_as_suffix: yes segments: - name: header type: header