Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ htmlcov

# Visual Studio Code settings
.vscode/
*.py,cover
13 changes: 12 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,15 @@ Gotchas
Supported macros: ifdef, ifndef, if, elif, define, undef, include, else,
pragma (only "once")

The #define directive supports both object-like and function-like macros:
* Object-like macros: `#define NAME value`
* Function-like macros: `#define NAME(params) body`
- Function-like macros must have '(' immediately after the macro name
- Supports zero or more parameters
- Arguments are expanded before substitution
- Nested macro calls are supported
- A macro name without '()' is not expanded (treated as identifier)

The #if and #elif directives support constant expression evaluation including:
* Integer constants
* Arithmetic operators: +, -, *, /, %
Expand All @@ -32,4 +41,6 @@ Limitations:
* Multiline continuations supported but whitespace handling may not be 1:1
with real preprocessors. Trailing whitespace is removed if before comment,
indentation from first line is removed
* Semi-colon handling may not be identical to real preprocessors
* Semi-colon handling may not be identical to real preprocessors
* Function-like macros do not support stringification (#) or
token pasting (##) operators
64 changes: 64 additions & 0 deletions simplecpreprocessor/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,14 @@ def constants_to_token_constants(constants):
TOKEN_CONSTANTS = constants_to_token_constants(platform.PLATFORM_CONSTANTS)


class FunctionLikeMacro:
"""Represents a function-like macro with parameters."""

def __init__(self, params, body):
self.params = params
self.body = body


class Defines:
def __init__(self, base):
self.defines = base.copy()
Expand Down Expand Up @@ -90,6 +98,62 @@ def process_define(self, **kwargs):
else: # pragma: no cover
# Defensive: should never happen as tokenizer ensures non-ws tokens
return

# Check if this is a function-like macro
# Function-like macros have '(' immediately after name (no whitespace)
if i+1 < len(chunk) and chunk[i+1].value == "(":
# Parse parameters
params = []
j = i + 2 # Start after '('
param_start = j
paren_depth = 0

while j < len(chunk):
token = chunk[j]
if token.value == "(" and not token.whitespace:
paren_depth += 1
elif token.value == ")" and not token.whitespace:
if paren_depth == 0:
# End of parameter list
# Add last parameter if any
if param_start < j:
param_tokens = chunk[param_start:j]
param_name = None
for pt in param_tokens:
if not pt.whitespace:
param_name = pt.value
break
if param_name:
params.append(param_name)
# Body starts after ')' and any whitespace
body_start = j + 1
while (body_start < len(chunk) and
chunk[body_start].whitespace):
body_start += 1
body = chunk[body_start:-1] # Exclude newline
self.defines[define_name] = FunctionLikeMacro(
params, body
)
return
else:
paren_depth -= 1
elif token.value == "," and paren_depth == 0:
# Parameter separator
param_tokens = chunk[param_start:j]
param_name = None
for pt in param_tokens:
if not pt.whitespace:
param_name = pt.value
break
if param_name:
params.append(param_name)
param_start = j + 1
j += 1

# If we get here, something went wrong
# Fall through to object-like macro handling

# Object-like macro
self.defines[define_name] = chunk[i+2:-1]

def process_endif(self, **kwargs):
Expand Down
246 changes: 246 additions & 0 deletions simplecpreprocessor/tests/test_function_macros.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
from __future__ import absolute_import
from simplecpreprocessor import preprocess
from simplecpreprocessor.filesystem import FakeFile


def run_case(input_list, expected):
ret = preprocess(input_list)
output = "".join(ret)
assert output == expected


def test_function_macro_simple():
"""Test basic function-like macro with one parameter."""
f_obj = FakeFile("header.h", [
"#define SQUARE(x) ((x) * (x))\n",
"SQUARE(5)\n"])
expected = "((5) * (5))\n"
run_case(f_obj, expected)


def test_function_macro_two_params():
"""Test function-like macro with two parameters."""
f_obj = FakeFile("header.h", [
"#define MAX(a, b) ((a) > (b) ? (a) : (b))\n",
"MAX(1, 2)\n"])
expected = "((1) > (2) ? (1) : (2))\n"
run_case(f_obj, expected)


def test_function_macro_three_params():
"""Test function-like macro with three parameters."""
f_obj = FakeFile("header.h", [
"#define ADD3(a, b, c) ((a) + (b) + (c))\n",
"ADD3(1, 2, 3)\n"])
expected = "((1) + (2) + (3))\n"
run_case(f_obj, expected)


def test_function_macro_no_params():
"""Test function-like macro with no parameters."""
f_obj = FakeFile("header.h", [
"#define FUNC() 42\n",
"FUNC()\n"])
expected = "42\n"
run_case(f_obj, expected)


def test_function_macro_with_expression():
"""Test function-like macro with expression arguments."""
f_obj = FakeFile("header.h", [
"#define DOUBLE(x) ((x) * 2)\n",
"DOUBLE(3 + 4)\n"])
expected = "((3 + 4) * 2)\n"
run_case(f_obj, expected)


def test_function_macro_not_called():
"""Test that function-like macro name without () is not expanded."""
f_obj = FakeFile("header.h", [
"#define SQUARE(x) ((x) * (x))\n",
"SQUARE\n"])
expected = "SQUARE\n"
run_case(f_obj, expected)


def test_function_macro_whitespace_before_paren():
"""Test function-like macro with whitespace before opening paren."""
f_obj = FakeFile("header.h", [
"#define SQUARE(x) ((x) * (x))\n",
"SQUARE (5)\n"])
# With whitespace before (, it should still be treated as a call
expected = "((5) * (5))\n"
run_case(f_obj, expected)


def test_object_like_macro_with_parens_in_body():
"""Test object-like macro with parentheses in body."""
f_obj = FakeFile("header.h", [
"#define FOO (x)\n",
"FOO\n"])
expected = "(x)\n"
run_case(f_obj, expected)


def test_function_macro_nested_calls():
"""Test nested function-like macro calls."""
f_obj = FakeFile("header.h", [
"#define DOUBLE(x) ((x) * 2)\n",
"DOUBLE(DOUBLE(3))\n"])
expected = "((((3) * 2)) * 2)\n"
run_case(f_obj, expected)


def test_function_macro_multiple_on_line():
"""Test multiple function-like macro calls on one line."""
f_obj = FakeFile("header.h", [
"#define ADD(a, b) ((a) + (b))\n",
"ADD(1, 2) ADD(3, 4)\n"])
expected = "((1) + (2)) ((3) + (4))\n"
run_case(f_obj, expected)


def test_function_macro_empty_arg():
"""Test function-like macro with empty argument."""
f_obj = FakeFile("header.h", [
"#define FUNC(x, y) x y\n",
"FUNC(a, )\n"])
# The space between x and y in the body is preserved
expected = "a \n"
run_case(f_obj, expected)


def test_function_macro_redefine():
"""Test redefining a function-like macro."""
f_obj = FakeFile("header.h", [
"#define FUNC(x) (x)\n",
"FUNC(1)\n",
"#undef FUNC\n",
"#define FUNC(x) ((x) * 2)\n",
"FUNC(2)\n"])
expected = "(1)\n((2) * 2)\n"
run_case(f_obj, expected)


def test_function_macro_nested_parens_in_params():
"""Test function-like macro with nested parentheses in parameter."""
f_obj = FakeFile("header.h", [
"#define FUNC(x) x\n",
"FUNC((a, b))\n"])
expected = "(a, b)\n"
run_case(f_obj, expected)


def test_function_macro_missing_args():
"""Test function-like macro with fewer arguments than parameters."""
f_obj = FakeFile("header.h", [
"#define FUNC(x, y, z) x y z\n",
"FUNC(a)\n"])
# Missing arguments are treated as empty
expected = "a \n"
run_case(f_obj, expected)


def test_function_macro_arg_with_trailing_whitespace():
"""Test function-like macro with whitespace in arguments."""
f_obj = FakeFile("header.h", [
"#define FUNC(x) [x]\n",
"FUNC( a )\n"])
expected = "[a]\n"
run_case(f_obj, expected)


def test_function_macro_unclosed_paren():
"""Test function-like macro with unclosed parenthesis.

When a macro call has no closing paren, it's not expanded.
"""
f_obj = FakeFile("header.h", [
"#define FUNC(x) [x]\n",
"FUNC(a\n"])
# Not expanded - treated as regular tokens
expected = "FUNC(a\n"
run_case(f_obj, expected)


def test_function_macro_malformed_definition():
"""Test malformed function-like macro definition.

When a macro definition has no closing paren in the parameter list,
it falls back to object-like macro behavior.
"""
f_obj = FakeFile("header.h", [
"#define FUNC(x\n",
"FUNC\n"])
# Falls back to object-like macro: FUNC is defined as "x"
expected = "x\n"
run_case(f_obj, expected)


def test_function_macro_whitespace_only_param():
"""Test function-like macro with whitespace-only parameter."""
f_obj = FakeFile("header.h", [
"#define FUNC( ) body\n",
"FUNC()\n"])
# Whitespace-only param is ignored, treated as zero params
expected = "body\n"
run_case(f_obj, expected)


def test_function_macro_trailing_comma_whitespace():
"""Test function-like macro with trailing comma and whitespace."""
f_obj = FakeFile("header.h", [
"#define FUNC(a, ) a\n",
"FUNC(1, 2)\n"])
# Second param is empty (whitespace only)
expected = "1\n"
run_case(f_obj, expected)


def test_function_macro_multiple_empty_params():
"""Test function-like macro with empty parameter in the middle."""
f_obj = FakeFile("header.h", [
"#define FUNC(a, , c) a c\n",
"FUNC(1, 2, 3)\n"])
# Second param is empty (whitespace only) so skipped
# Macro has params [a, c], invoked with args [1, 2, 3]
expected = "1 2\n"
run_case(f_obj, expected)


def test_function_macro_nested_parens_in_definition():
"""Test function-like macro with nested parens in parameter list.

This is invalid C. The parser extracts '(' as the parameter name
due to the way it finds the first non-whitespace token.
"""
f_obj = FakeFile("header.h", [
"#define FUNC((x)) x\n",
"FUNC((5))\n"])
# Parameter is parsed as '(', body is 'x'
# When called, '(' is not found in the arguments, so body 'x' is output
expected = "x\n"
run_case(f_obj, expected)


def test_function_macro_deeply_nested_parens_in_definition():
"""Test function-like macro with deeply nested parens in definition.

This exercises the paren_depth tracking in parameter parsing.
"""
f_obj = FakeFile("header.h", [
"#define FUNC(((a))) body\n",
"FUNC()\n"])
# Parens are tracked, parameter extracted correctly
expected = "body\n"
run_case(f_obj, expected)


def test_function_macro_trailing_comma_no_whitespace():
"""Test function-like macro with trailing comma and no whitespace."""
f_obj = FakeFile("header.h", [
"#define FUNC(x, y) x y\n",
"FUNC(a,)\n"])
# Second arg is completely empty (no whitespace)
expected = "a \n"
run_case(f_obj, expected)
Loading