Packages

C lexer for the Makeup syntax highlighter.

Current section

Files

Jump to
makeup_c lib makeup_c.ex
Raw

lib/makeup_c.ex

defmodule Makeup.Lexers.CLexer do
import NimbleParsec
import Makeup.Lexer.Combinators
import Makeup.Lexer.Groups
import Makeup.Lexers.CLexer.Helper
@behaviour Makeup.Lexer
###################################################################
# Step #1: tokenize the input (into a list of tokens)
###################################################################
# We will often compose combinators into larger combinators.
# Sometimes, the smaller combinator is usefull on its own as a token, and sometimes it isn't.
# We'll adopt the following "convention":
#
# 1. A combinator that ends with `_name` returns a string
# 2. Other combinators will *usually* return a token
#
# Why this convention? Tokens can't be composed further, while raw strings can.
# This way, we immediately know which of the combinators we can compose.
# TODO: check we're following this convention
whitespace = ascii_string([?\r, ?\s, ?\n, ?\f], min: 1) |> token(:whitespace)
any_char = utf8_char([]) |> token(:error)
# Numbers
digits = ascii_string([?0..?9], min: 1)
bin_digits = ascii_string([?0..?1], min: 1)
hex_digits = ascii_string([?0..?9, ?a..?f, ?A..?F], min: 1)
oct_digits = ascii_string([?0..?7], min: 1)
# Digits in an integer may be separated by underscores
number_bin_part = with_optional_separator(bin_digits, "_")
number_oct_part = with_optional_separator(oct_digits, "_")
number_hex_part = with_optional_separator(hex_digits, "_")
integer = with_optional_separator(digits, "_")
# Tokens for the lexer
number_bin = string("0b") |> concat(number_bin_part) |> token(:number_bin)
number_oct = string("0o") |> concat(number_oct_part) |> token(:number_oct)
number_hex = string("0x") |> concat(number_hex_part) |> token(:number_hex)
# Base 10
number_integer = token(integer, :number_integer)
# Floating point numbers
float_scientific_notation_part =
ascii_string([?e, ?E], 1)
|> optional(string("-"))
|> concat(integer)
number_float =
integer
|> string(".")
|> concat(integer)
|> optional(float_scientific_notation_part)
|> token(:number_float)
# Yes, Elixir supports much more than this.
# TODO: adapt the code from the official tokenizer, which parses the unicode database
variable_name =
ascii_string([?a..?z, ?_], 1)
|> optional(ascii_string([?a..?z, ?_, ?0..?9, ?A..?Z], min: 1))
|> optional(ascii_string([??, ?!], 1))
# Can also be a function name
variable =
variable_name
|> lexeme
|> token(:name)
define_name =
ascii_string([?A..?Z], 1)
|> optional(ascii_string([?a..?z, ?_, ?0..?9, ?A..?Z], min: 1))
define = token(define_name, :name_constant)
operator_name = word_from_list(~W(
-> + - * / % ++ -- ~ ^ & && | ||
= += -= *= /= &= |= %= ^= << >>
<<= >>= > < >= <= == != ! ? :
))
operator = token(operator_name, :operator)
normal_char =
string("?")
|> utf8_string([], 1)
|> token(:string_char)
escape_char =
string("?\\")
|> utf8_string([], 1)
|> token(:string_char)
directive =
string("#")
|> concat(variable_name)
|> token(:keyword_pseudo)
punctuation =
word_from_list(
["\\\\", ":", ";", ",", "."],
:punctuation
)
delimiters_punctuation =
word_from_list(
~W( ( \) [ ] { }),
:punctuation
)
comment = many_surrounded_by(parsec(:root_element), "/*", "*/")
delimiter_pairs = [
delimiters_punctuation,
comment
]
normal_atom_name =
utf8_string([?A..?Z, ?a..?z, ?_], 1)
|> optional(utf8_string([?A..?Z, ?a..?z, ?_, ?0..?9, ?@], min: 1))
# normal_atom =
# string(":")
# |> choice([operator_name, normal_atom_name])
# |> token(:string_symbol)
unicode_char_in_string =
string("\\u")
|> ascii_string([?0..?9, ?a..?f, ?A..?F], 4)
|> token(:string_escape)
escaped_char =
string("\\")
|> utf8_string([], 1)
|> token(:string_escape)
combinators_inside_string = [
unicode_char_in_string,
escaped_char
]
string_keyword =
choice([
string_like("\"", "\"", combinators_inside_string, :string_symbol),
string_like("'", "'", combinators_inside_string, :string_symbol)
])
|> concat(token(string(":"), :punctuation))
normal_keyword =
choice([operator_name, normal_atom_name])
|> token(:string_symbol)
|> concat(token(string(":"), :punctuation))
keyword =
choice([
normal_keyword,
string_keyword
])
|> concat(whitespace)
double_quoted_string_interpol = string_like("\"", "\"", combinators_inside_string, :string)
line = repeat(lookahead_not(ascii_char([?\n])) |> utf8_string([], 1))
inline_comment =
string("//")
|> concat(line)
|> token(:comment_single)
multiline_comment = string_like("/*", "*/", combinators_inside_string, :comment_multiline)
root_element_combinator =
choice(
[
whitespace,
# Comments
multiline_comment,
inline_comment,
# Syntax sugar for keyword lists (must come before variables and strings)
directive,
keyword,
# Strings
double_quoted_string_interpol
] ++
[
# Chars
escape_char,
normal_char
] ++
delimiter_pairs ++
[
# Operators
operator,
# Numbers
number_bin,
number_oct,
number_hex,
# Floats must come before integers
number_float,
number_integer,
# Names
variable,
define,
punctuation,
# If we can't parse any of the above, we highlight the next character as an error
# and proceed from there.
# A lexer should always consume any string given as input.
any_char
]
)
# By default, don't inline the lexers.
# Inlining them increases performance by ~20%
# at the cost of doubling the compilation times...
@inline false
@doc false
def __as_c_language__({ttype, meta, value}) do
{ttype, Map.put(meta, :language, :c), value}
end
# Semi-public API: these two functions can be used by someone who wants to
# embed an Elixir lexer into another lexer, but other than that, they are not
# meant to be used by end-users.
# @impl Makeup.Lexer
defparsec(
:root_element,
root_element_combinator |> map({__MODULE__, :__as_c_language__, []}),
inline: @inline
)
# @impl Makeup.Lexer
defparsec(
:root,
repeat(parsec(:root_element)),
inline: @inline
)
###################################################################
# Step #2: postprocess the list of tokens
###################################################################
@keyword ~W[
alignas alignoif asm atomic_cancel atomic_commit
atomic_noexcept auto break case catch class co_await
co_return co_yield compl concept const const_cast
constexpr continue decltype default delete do dynamic_cast
else enum explicit export extern for friend goto if
import inline module mutable namespace new noexcept
nullptr operator private protected public register
reinterpret_cast requires return sizeof static static_assert
static_cast struct switch synchronized template this
thread_local throw try typedef typeid typename union
using virtual volatile while
]
@keyword_type ~W[
bool byte int long unsigned double char short signed float wchar_t
char16_t char32_t int8_t uint8_t int16_t uint16_t int32_t uint32_t
int64_t uint64_t
]
@keyword_constant ~W[
NULL true false void
]
@operator_word ~W[and and_eq bitand bitor not not_eq or or_eq xor xor_eq]
@name_builtin_pseudo ~W[__FUNCTION__ __FILE__ __LINE__]
# The `postprocess/1` function will require a major redesign when we decide to support
# custom `def`-like keywords supplied by the user.
defp postprocess_helper([]), do: []
# match function names. They are followed by parens...
defp postprocess_helper([
{:name, attrs, text},
{:punctuation, %{language: :c}, "("}
| tokens
]) do
[
{:name_function, attrs, text},
{:punctuation, %{language: :c}, "("}
| postprocess_helper(tokens)
]
end
defp postprocess_helper([{:name, attrs, text} | tokens]) when text in @keyword,
do: [{:keyword, attrs, text} | postprocess_helper(tokens)]
defp postprocess_helper([{:name, attrs, text} | tokens]) when text in @keyword_type,
do: [{:keyword_type, attrs, text} | postprocess_helper(tokens)]
defp postprocess_helper([{:name, attrs, text} | tokens]) when text in @keyword_constant,
do: [{:keyword_constant, attrs, text} | postprocess_helper(tokens)]
defp postprocess_helper([{:name, attrs, text} | tokens]) when text in @operator_word,
do: [{:operator_word, attrs, text} | postprocess_helper(tokens)]
defp postprocess_helper([{:name, attrs, text} | tokens]) when text in @name_builtin_pseudo,
do: [{:name_builtin_pseudo, attrs, text} | postprocess_helper(tokens)]
# Unused variables
defp postprocess_helper([{:name, attrs, "_" <> _name = text} | tokens]),
do: [{:comment, attrs, text} | postprocess_helper(tokens)]
# Otherwise, don't do anything with the current token and go to the next token.
defp postprocess_helper([token | tokens]), do: [token | postprocess_helper(tokens)]
# Public API
@impl Makeup.Lexer
def postprocess(tokens, _opts \\ []), do: postprocess_helper(tokens)
###################################################################
# Step #3: highlight matching delimiters
###################################################################
@impl Makeup.Lexer
defgroupmatcher(:match_groups,
parentheses: [
open: [[{:punctuation, %{language: :c}, "("}]],
close: [[{:punctuation, %{language: :c}, ")"}]]
],
array: [
open: [[{:punctuation, %{language: :c}, "["}]],
close: [[{:punctuation, %{language: :c}, "]"}]]
],
brackets: [
open: [[{:punctuation, %{language: :c}, "{"}]],
close: [[{:punctuation, %{language: :c}, "}"}]]
]
)
defp remove_initial_newline([{ttype, meta, text} | tokens]) do
case to_string(text) do
"\n" -> tokens
"\n" <> rest -> [{ttype, meta, rest} | tokens]
end
end
# Finally, the public API for the lexer
@impl Makeup.Lexer
def lex(text, opts \\ []) do
group_prefix = Keyword.get(opts, :group_prefix, random_prefix(10))
{:ok, tokens, "", _, _, _} = root("\n" <> text)
tokens
|> remove_initial_newline()
|> postprocess([])
|> match_groups(group_prefix)
end
end