Skip to content

Commit

Permalink
Add support for local scopes (#scope ... #endscope) (#96)
Browse files Browse the repository at this point in the history
  • Loading branch information
fpottier authored Nov 29, 2024
1 parent 0208328 commit aff8a12
Show file tree
Hide file tree
Showing 16 changed files with 401 additions and 30 deletions.
4 changes: 3 additions & 1 deletion Changes.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
## v1.7.1 (2024-??-??)
## v1.8.0 (2024-??-??)
- [+ui] A scope, delimited by `#scope ... #endscope`,
limits the effect of `#define`, `#def ... #enddef`, and `#undef`.
- [bug] Fix `cppo -version`, which used to print a blank line (#92).

## v1.7.0 (2024-08-22)
Expand Down
54 changes: 54 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ by a valid directive name or by a number:

```ocaml
BLANK* "#" BLANK* ("def"|"enddef"|"define"|"undef"
|"scope"|"endscope"
|"if"|"ifdef"|"ifndef"|"else"|"elif"|"endif"
|"include"
|"warning"|"error"
Expand Down Expand Up @@ -287,6 +288,40 @@ let forty_two =
(let x = (1+2+3+4+5+6) in (x + x))
```

Scopes
------
When a block of text is delimited by `#scope ... #endscope`,
all macro definitions (`#define`, `#def ... #enddef`)
and undefinitions (`#undef`)
become local:
they take effect only within this block.

```ocaml
(* Here, assume that the macro FOO is not defined. *)
#scope
#define FOO "FOO is now defined"
let x = FOO (* FOO expands to "FOO is now defined" *)
#endscope
(* Here, the macro FOO is again not defined. *)
#define FOO 42
let y = FOO (* FOO expands to 42 *)
```

Scopes can be nested,
as illustrated by this example:

```ocaml
#scope
#define HELLO "Hello, "
#scope
#define MAN "man"
let message1 = HELLO ^ MAN
#endscope
(* Here, MAN is no longer defined, but HELLO still is. *)
let message2 = HELLO ^ "world"
#endscope
```

Conditionals
------------

Expand Down Expand Up @@ -607,6 +642,25 @@ and`.mlpack` files. The following tags are available:
* `cppo_V_OCAML``-V OCAML:VERSION`, where `VERSION`
is the version of OCaml that ocamlbuild uses.

Balancing delimiters
--------------------

All delimiters,
including scope delimiters (`#scope` and `#endscope`),
delimiters of macro definitions (`#def` and `#enddef`),
and delimiters of conditional constructs (`#if`, `#endif`, etc.),
must be used in a well-balanced manner.

This requirement does *not* apply separately to each category of delimiters.
instead, it applies to all categories of delimiters at once.
This is a stricter requirement.
Thus, for example, `#scope` cannot be followed with `#endif`,
and `#if` cannot be followed with `#endscope`.
In other words,
a scope cannot contain a fragment of a conditional construct,
and a conditional construct cannot contain a fragment of a macro definition.


Detailed command-line usage and options
---------------------------------------

Expand Down
8 changes: 8 additions & 0 deletions src/cppo_eval.ml
Original file line number Diff line number Diff line change
Expand Up @@ -624,6 +624,14 @@ and expand_node ?(top = false) g env0 (x : node) =
else
M.add name (EDef (loc, formals, body, env0)) env0

| `Scope body ->
(* A [body] is just a [node]. We expand this node, and drop
the resulting environment; instead, we return the current
environment. *)
let env = expand_node ~top g env0 body in
ignore env;
env0

| `Undef (loc, name) ->
g.require_location := true;
if is_reserved name then
Expand Down
12 changes: 12 additions & 0 deletions src/cppo_lexer.mll
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,18 @@ and directive e = parse
{ blank_until_eol e lexbuf;
UNDEF (long_loc e, id) }

(* #scope opens a block, which we expect will be ended by #endscope.
It does not set [e.in_directive], so backslashes and newlines do
not receive special treatment. *)
| blank* "scope" dblank0
{ e.in_directive <- false;
SCOPE (long_loc e) }

(* #endscope ends a block. *)
| blank* "endscope"
{ blank_until_eol e lexbuf;
ENDSCOPE (long_loc e) }

| blank* "if" dblank1 { e.lexer <- `Test;
IF (long_loc e) }
| blank* "elif" dblank1 { e.lexer <- `Test;
Expand Down
12 changes: 11 additions & 1 deletion src/cppo_parser.mly
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
%token < Cppo_types.loc * string option * int > LINE
%token < Cppo_types.loc * Cppo_types.bool_expr > IFDEF
%token < Cppo_types.loc * string * string > EXT
%token < Cppo_types.loc > ENDEF IF ELIF ELSE ENDIF ENDTEST
%token < Cppo_types.loc > ENDEF SCOPE ENDSCOPE IF ELIF ELSE ENDIF ENDTEST

/* Boolean expressions in #if/#elif directives */
%token TRUE FALSE DEFINED NOT AND OR EQ LT GT NE LE GE
Expand Down Expand Up @@ -135,6 +135,16 @@ node:
/* We include this rule in order to produce a good error message
when a #def has no matching #enddef. */

| SCOPE body ENDSCOPE
{ let body = `Seq $2 in
`Scope body }

| SCOPE body EOF
{ let loc = $1 in
error loc "This #scope is never closed: perhaps #endscope is missing" }
/* We include this rule in order to produce a good error message
when a #scope has no matching #endscope. */

| UNDEF
{ `Undef $1 }
| WARNING
Expand Down
5 changes: 4 additions & 1 deletion src/cppo_types.ml
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ type node =
| `Def of (loc * macro * formals * body)
(* the list [formals] is empty if and only if no parentheses
are used at this macro definition site. *)
| `Scope of body
| `Undef of (loc * macro)
| `Include of (loc * string)
| `Ext of (loc * string * string)
Expand Down Expand Up @@ -146,7 +147,7 @@ let warning loc s =

let dummy_loc = (Lexing.dummy_pos, Lexing.dummy_pos)

let node_loc (node : node) : loc =
let rec node_loc (node : node) : loc =
match node with
| `Ident (loc, _, _)
| `Def (loc, _, _, _)
Expand All @@ -162,6 +163,8 @@ let node_loc (node : node) : loc =
| `Current_line loc
| `Current_file loc
-> loc
| `Scope node ->
node_loc node
| `Stringify _
| `Capitalize _
| `Concat (_, _)
Expand Down
1 change: 1 addition & 0 deletions src/cppo_types.mli
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ type node =
| `Def of (loc * macro * formals * body)
(* the list [formals] is empty if and only if no parentheses
are used at this macro definition site. *)
| `Scope of body
| `Undef of (loc * macro)
| `Include of (loc * string)
| `Ext of (loc * string * string)
Expand Down
21 changes: 16 additions & 5 deletions test/def.cppo
Original file line number Diff line number Diff line change
Expand Up @@ -44,34 +44,45 @@ let forty_two = APPLY(ID, C )
)
#enddef

(* In the examples that follow, #scope ... #endscope is used to
avoid the need to #undefine local macros such as BODY and F. *)

(* Iteration over an array, with a normal loop. *)
let iter f a =
#scope
#define BODY(i) (f a.(i))
LOOP(0, Array.length a, BODY)
#undef BODY
#endscope

(* Iteration over an array, with an unrolled loop. *)
let unrolled_iter f a =
#scope
#define BODY(i) (f a.(i))
UNROLLED_LOOP(0, Array.length a, BODY)
#undef BODY
#endscope

(* Printing an array, with a normal loop. *)
let print_int_array a =
#scope
#define F(i) Printf.printf "%d" a.(i)
LOOP(0, Array.length a, F)
#endscope

(* A higher-order macro that produces a definition of [iter],
and accepts an arbitrary definition of the macro [LOOP]. *)
#define BODY(i) (f a.(i))
#def DEFINE_ITER(iter, LOOP : [..[.]])
#scope
#define BODY(i) (f a.(i))
let iter f a =
LOOP(0, Array.length a, BODY)
LOOP(0, Array.length a, BODY)
#endscope
#enddef
#undef BODY

(* Some noise, which does not affect the above definitions. *)
#define BODY(i) "noise"

DEFINE_ITER(iter, LOOP)
DEFINE_ITER(unrolled_iter, UNROLLED_LOOP)

(* Just because we can, undefine BODY. *)
#undef BODY
33 changes: 22 additions & 11 deletions test/def.ref
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,14 @@ let forty_two =
(* A [for]-loop macro that performs unrolling. *)

# 47 "def.cppo"
(* In the examples that follow, #scope ... #endscope is used to
avoid the need to #undefine local macros such as BODY and F. *)

(* Iteration over an array, with a normal loop. *)
let iter f a =


# 50 "def.cppo"
# 54 "def.cppo"

(
for __index = 0 to Array.length a-1 do
Expand All @@ -40,11 +44,12 @@ let iter f a =
)


# 53 "def.cppo"
# 57 "def.cppo"
(* Iteration over an array, with an unrolled loop. *)
let unrolled_iter f a =


# 56 "def.cppo"
# 61 "def.cppo"
(
(* #define can be nested inside #def. *)
(* #def can be nested inside #def. *)
Expand Down Expand Up @@ -75,11 +80,12 @@ let unrolled_iter f a =
)


# 59 "def.cppo"
# 64 "def.cppo"
(* Printing an array, with a normal loop. *)
let print_int_array a =


# 62 "def.cppo"
# 68 "def.cppo"

(
for __index = 0 to Array.length a-1 do
Expand All @@ -88,28 +94,30 @@ let print_int_array a =
)


# 64 "def.cppo"
# 71 "def.cppo"
(* A higher-order macro that produces a definition of [iter],
and accepts an arbitrary definition of the macro [LOOP]. *)

# 73 "def.cppo"
# 81 "def.cppo"
(* Some noise, which does not affect the above definitions. *)

# 76 "def.cppo"
# 84 "def.cppo"


let iter f a =

(
for __index = 0 to Array.length a-1 do
(f a.(__index))
done
)


# 77 "def.cppo"
# 85 "def.cppo"


let unrolled_iter f a =
(
(
(* #define can be nested inside #def. *)
(* #def can be nested inside #def. *)
let __finish = ( Array.length a) in
Expand Down Expand Up @@ -139,3 +147,6 @@ let print_int_array a =
)



# 87 "def.cppo"
(* Just because we can, undefine BODY. *)
17 changes: 17 additions & 0 deletions test/dune
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,11 @@
(deps (:< lexical.cppo))
(action (with-stdout-to %{targets} (run %{bin:cppo} %{<}))))

(rule
(targets scope.out)
(deps (:< scope.cppo))
(action (with-stdout-to %{targets} (run %{bin:cppo} %{<}))))

(rule
(targets higher_order_macros.out)
(deps (:< higher_order_macros.cppo))
Expand Down Expand Up @@ -136,6 +141,9 @@
(rule (alias runtest) (package cppo)
(action (diff lexical.ref lexical.out)))

(rule (alias runtest) (package cppo)
(action (diff scope.ref scope.out)))

(rule (alias runtest) (package cppo)
(action (diff higher_order_macros.ref higher_order_macros.out)))

Expand Down Expand Up @@ -273,3 +281,12 @@

(rule (alias runtest) (package cppo)
(action (diff missing_enddef.ref missing_enddef.err)))

(rule
(targets missing_endscope.err)
(deps (:< missing_endscope.cppo))
(action (with-stderr-to %{targets}
(with-accepted-exit-codes (not 0) (run %{bin:cppo} %{<})))))

(rule (alias runtest) (package cppo)
(action (diff missing_endscope.ref missing_endscope.err)))
9 changes: 7 additions & 2 deletions test/higher_order_macros.cppo
Original file line number Diff line number Diff line change
Expand Up @@ -32,17 +32,22 @@ let forty_two = APPLY(ID, C )
done\
)

(* In some of the examples that follow, #scope ... #endscope is used
to avoid the need to #undefine local macros such as BODY and F. *)

(* Iteration over an array, with a normal loop. *)
let iter f a =
#scope
#define BODY(i) (f a.(i))
LOOP(0, Array.length a, BODY)
#undef BODY
#endscope

(* Iteration over an array, with an unrolled loop. *)
let unrolled_iter f a =
#scope
#define BODY(i) (f a.(i))
UNROLLED_LOOP(0, Array.length a, BODY)
#undef BODY
#endscope

(* Printing an array, with a normal loop. *)
let print_int_array a =
Expand Down
Loading

0 comments on commit aff8a12

Please sign in to comment.