defmodule Zig.Parser do require Pegasus import NimbleParsec defstruct [:doc_comment, code: [], dependencies: [], comments: []] alias Zig.Parser.Asm alias Zig.Parser.AssignExpr alias Zig.Parser.Block alias Zig.Parser.Collected alias Zig.Parser.ContainerDecl alias Zig.Parser.ContainerDeclarations alias Zig.Parser.Decl alias Zig.Parser.ErrorUnionExpr alias Zig.Parser.VarDecl alias Zig.Parser.Expr alias Zig.Parser.For alias Zig.Parser.InitList alias Zig.Parser.Test alias Zig.Parser.ComptimeDecl alias Zig.Parser.Function alias Zig.Parser.PrimaryExpr alias Zig.Parser.PrimaryTypeExpr alias Zig.Parser.Statement alias Zig.Parser.TypeExpr alias Zig.Parser.ParamDecl alias Zig.Parser.ParseError @keywords ~w[addrspace align allowzero and anyframe anytype asm async await break callconv catch comptime const continue defer else enum errdefer error export extern fn for if inline noalias nosuspend noinline opaque or orelse packed pub resume return linksection struct suspend switch test threadlocal try union unreachable usingnamespace var volatile while]a @keyword_mapping Enum.map(@keywords, &{:"KEYWORD_#{&1}", [token: &1]}) @sub_operators %{ AMPERSAND: :&, AMPERSANDEQUAL: :"&=", ASTERISK: :*, ASTERISK2: :**, ASTERISKEQUAL: :"*=", ASTERISKPERCENT: :"*%", ASTERISKPERCENTEQUAL: :"*%=", ASTERISKPIPE: :"*|", ASTERISKPIPEEQUAL: :"*|=", CARET: :^, CARETEQUAL: :"^=", DOT3: :..., DOTASTERISK: :".*", DOTQUESTIONMARK: :".?", EQUAL: :=, EQUALEQUAL: :==, EQUALRARROW: :"=>", EXCLAMATIONMARK: :!, EXCLAMATIONMARKEQUAL: :!=, LARROW: :<, LARROW2: :"<<", LARROW2EQUAL: :"<<=", LARROW2PIPE: :"<<|", LARROW2PIPEEQUAL: :"<<|=", LARROWEQUAL: :<=, MINUS: :-, MINUSEQUAL: :"-=", MINUSPERCENT: :"-%", MINUSPERCENTEQUAL: :"-%=", MINUSPIPE: :"-|", MINUSPIPEEQUAL: :"-|", PERCENT: :%, PERCENTEQUAL: :"%=", PIPE: :|, PIPE2: :||, PIPEEQUAL: :"|=", PLUS: :+, PLUS2: :++, PLUSEQUAL: :"+=", PLUSPERCENT: :"+%", PLUSPERCENTEQUAL: :"+%=", PLUSPIPE: :"+|", PLUSPIPEEQUAL: :"+|=", RARROW: :>, RARROW2: :">>", RARROW2EQUAL: :">>=", RARROWEQUAL: :>=, SLASH: :/, SLASHEQUAL: :"/=", TILDE: :"~" } @sub_operator_mapping Enum.map(@sub_operators, fn {name, op} -> {name, [token: op, start_position: true]} end) @operators ~w[COMMA DOT DOT2 COLON LBRACE LBRACKET LPAREN MINUSRARROW LETTERC QUESTIONMARK RBRACE RBRACKET RPAREN SEMICOLON]a @operator_mapping Enum.map(@operators, &{&1, [token: true]}) @collecteds ~w[IDENTIFIER INTEGER FLOAT STRINGLITERAL BUILTINIDENTIFIER line_string]a @collected_mapping Enum.map( @collecteds, &{&1, [collect: true, post_traverse: {Collected, :post_traverse, [&1]}]} ) @lists ~w[IdentifierList SwitchProngList AsmOutputList AsmInputList StringList ParamDeclList ExprList]a @lists_mapping Enum.map(@lists, &{&1, tag: true}) @parser_options [ container_doc_comment: [ tag: :doc_comment, collect: true ], char_escape: [ post_traverse: :char_escape, tag: true, collect: true ], doc_comment: [post_traverse: :doc_comment, tag: true, collect: true], line_comment: [post_traverse: :line_comment, tag: true, start_position: true], AssignExpr: [ tag: true, post_traverse: {AssignExpr, :post_traverse, []} ], TestDecl: [ tag: true, start_position: true, post_traverse: {Test, :post_traverse, []} ], Decl: [ tag: true, post_traverse: {Decl, :post_traverse, []} ], ContainerDecl: [ start_position: true, tag: true, post_traverse: {ContainerDecl, :post_traverse, []} ], ContainerDeclarations: [ tag: true, post_traverse: {ContainerDeclarations, :post_traverse, []} ], PrimaryExpr: [ start_position: true, tag: true, post_traverse: {PrimaryExpr, :post_traverse, []} ], skip: [ignore: true], STRINGLITERALSINGLE: [ tag: true, post_traverse: :string_literal_single ], CHAR_LITERAL: [ post_traverse: {Collected, :post_traverse, [:CHAR_LITERAL]} ], ComptimeDecl: [ tag: true, post_traverse: {ComptimeDecl, :post_traverse, []} ], TypeExpr: [ tag: true, post_traverse: {TypeExpr, :post_traverse, []} ], Expr: [ tag: true, post_traverse: {Expr, :post_traverse, []} ], ErrorUnionExpr: [ tag: true, post_traverse: {ErrorUnionExpr, :post_traverse, []} ], InitList: [ tag: true, post_traverse: {InitList, :post_traverse, []} ], ParamDecl: [tag: true, post_traverse: {ParamDecl, :post_traverse, []}], VarDecl: [ start_position: true, tag: true, post_traverse: {VarDecl, :post_traverse, []} ], PrimaryTypeExpr: [ tag: true, post_traverse: {PrimaryTypeExpr, :post_traverse, []} ], SwitchItem: [tag: true], AsmExpr: [ start_position: true, tag: true, post_traverse: {Asm, :post_traverse, []} ], BlockExpr: [ start_position: true, tag: true, post_traverse: {Block, :post_traverse, []} ], Block: [ start_position: true, tag: true, post_traverse: {Block, :post_traverse, []} ], FnProto: [ start_position: true, tag: true, post_traverse: {Function, :post_traverse, []} ], ForStatement: [tag: true, post_traverse: {For, :post_traverse, []}], Statement: [tag: true, post_traverse: {Statement, :post_traverse, []}], # keywords that add inline LoopStatement: [tag: true, post_traverse: :add_inline], # basic pseudofunction keywords CallConv: [tag: true, post_traverse: :pseudofunction], ByteAlign: [tag: true, post_traverse: :pseudofunction], LinkSection: [tag: true, post_traverse: :pseudofunction], AddrSpace: [tag: true, post_traverse: :pseudofunction], # substituted functions: mb_utf8_literal: [alias: :utf8], # Top level Root: [tag: true, post_traverse: :post_traverse] ] ++ @keyword_mapping ++ @operator_mapping ++ @sub_operator_mapping ++ @collected_mapping ++ @lists_mapping Pegasus.parser_from_file(Path.join(__DIR__, "grammar/grammar.y"), @parser_options) zig_parser = empty() |> post_traverse(empty(), :init) |> parsec(:Root) defparsecp(:parser, zig_parser) defparsecp(:utf8, utf8_char(not: 0)) def parse(string) do case parser(string) do {:ok, _, "", parser, _, _} -> %{ parser | comments: Enum.reverse(parser.comments), dependencies: Enum.uniq(parser.dependencies) } {:error, message, remainder, context, line, column} -> raise ParseError, message: message, remainder: remainder, context: context, line: line, column: column end end # parser combinators defp init(code, args, context, _, _) do {code, args, struct(__MODULE__, context)} end defp doc_comment(rest, [{:doc_comment, [comment]} | rest_args], context, _, _) do {rest, [{:doc_comment, trim_doc_comment(comment, "///")} | rest_args], context} end defp line_comment( rest, [{:line_comment, [position, "//" | comment]} | rest_args], context, _, _ ) do comment_data = {IO.iodata_to_binary(comment), position} {rest, rest_args, %{context | comments: [comment_data | context.comments]}} end defp line_comment( rest, [{:line_comment, [position, "////" | comment]} | rest_args], context, _, _ ) do comment_data = {IO.iodata_to_binary(["//", comment]), position} {rest, rest_args, %{context | comments: [comment_data | context.comments]}} end defp char_escape(rest, [{:char_escape, [escape_string]} | rest_args], context, _, _) do {rest, [process_escape(escape_string) | rest_args], context} end defp trim_doc_comment(doc_comment, separator) do doc_comment |> String.split("\n") |> Enum.map(&String.trim/1) |> Enum.map(&String.trim_leading(&1, separator)) |> Enum.join("\n") end defp string_literal_single(rest, [{:STRINGLITERALSINGLE, literal} | rest_args], context, _, _) do remove_ends = Enum.slice(literal, 1..-2//1) {rest, [List.to_string(remove_ends) | rest_args], context} end def post_traverse("", [Root: [{:doc_comment, [doc_comment]} | code]], context, _, _) do doc_comment = trim_doc_comment(doc_comment, "//!") {"", [], struct(context, code: code, doc_comment: doc_comment)} end def post_traverse("", [Root: code], context, _, _) do {"", [], struct(context, code: code)} end @escaped %{?t => ?\t, ?n => ?\n, ?' => ?', ?" => ?", ?\\ => ?\\} defp process_escape(<<92, char>>), do: @escaped[char] defp process_escape("\\u{" <> what) do what |> String.trim_trailing("}") |> String.to_integer(16) end defp process_escape(<<"\\x"::binary, number::binary-2>>) do String.to_integer(number) end defp add_inline(rest, [{_tag, [:inline, payload]} | rest_args], context, _loc, _col) do {rest, [%{payload | inline: true} | rest_args], context} end defp pseudofunction( rest, [{_tag, [name, :LPAREN, payload, :RPAREN]} | rest_args], context, _loc, _col ) do {rest, [{name, payload} | rest_args], context} end @doc false def put_location(%_{} = struct, location) do Map.replace!(struct, :location, {location.line, location.column}) end end