├── .gitignore ├── test ├── dune ├── include.t ├── if-ocaml-version.t ├── cli.t └── defines.t ├── .ocamlformat ├── dune ├── dune-project ├── mucppo.opam ├── README.md ├── mucppo.ml └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | _opam/ 2 | _build/ 3 | -------------------------------------------------------------------------------- /test/dune: -------------------------------------------------------------------------------- 1 | (cram 2 | (deps %{bin:mucppo})) 3 | -------------------------------------------------------------------------------- /.ocamlformat: -------------------------------------------------------------------------------- 1 | version=0.25.1 2 | profile=conventional 3 | -------------------------------------------------------------------------------- /dune: -------------------------------------------------------------------------------- 1 | (executable 2 | (public_name mucppo) 3 | (modules mucppo)) 4 | -------------------------------------------------------------------------------- /test/include.t: -------------------------------------------------------------------------------- 1 | File inclusion should work. 2 | 3 | $ cat > included.ml < let included = () 5 | > EOF 6 | $ cat > including.ml < #include "included.ml" 8 | > EOF 9 | $ mucppo < including.ml 10 | let included = () 11 | 12 | More complex inclusions should also work: 13 | 14 | $ cat > included.ml < #if OCAML_VERSION >= (1, 0, 0) 16 | > OCaml 17 | > #endif 18 | > EOF 19 | $ mucppo < including.ml 20 | OCaml 21 | -------------------------------------------------------------------------------- /dune-project: -------------------------------------------------------------------------------- 1 | (lang dune 3.7) 2 | (name mucppo) 3 | (cram enable) 4 | 5 | (generate_opam_files true) 6 | 7 | (source (github Leonidas-from-XIV/mucppo)) 8 | (maintainers "marek@xivilization.net") 9 | (license "CC0-1.0") 10 | 11 | (package 12 | (name mucppo) 13 | (synopsis "Minimal, embeddable subset of CPPO") 14 | (description "\ 15 | This package provides a single-file no-dependency reimplementation of a 16 | subset of CPPO that is meant to be used in packages that use dune.") 17 | (depends 18 | (ocaml (>= 4.02.3)))) 19 | -------------------------------------------------------------------------------- /test/if-ocaml-version.t: -------------------------------------------------------------------------------- 1 | Simple test for futuristic OCaml version 2 | 3 | $ cat > high-version.ml < #if OCAML_VERSION >= (3000, 0, 0) 5 | > OCaml3k 6 | > #else 7 | > OCaml 8 | > #endif 9 | > EOF 10 | $ mucppo < high-version.ml 11 | OCaml 12 | 13 | Also for a very old version 14 | 15 | $ cat > low-version.ml < #if OCAML_VERSION >= (1, 0, 0) 17 | > OCaml 1.0.0 or newer 18 | > #else 19 | > OCaml the ancient 20 | > #endif 21 | > EOF 22 | $ mucppo < low-version.ml 23 | OCaml 1.0.0 or newer 24 | -------------------------------------------------------------------------------- /test/cli.t: -------------------------------------------------------------------------------- 1 | Testing some very basic command line handling, roughly compatible with CPPO. 2 | 3 | This is the file we'll work on 4 | 5 | $ cat > input.ml < let () = print_endline "Hello world" 7 | > EOF 8 | 9 | We might have a help output? 10 | 11 | $ mucppo --help 12 | mucppo -o 13 | -o Set output file name 14 | -help Display this list of options 15 | --help Display this list of options 16 | 17 | It should support input via a filename and output via stdout: 18 | 19 | $ mucppo input.ml 20 | let () = print_endline "Hello world" 21 | 22 | It should also support specifying the output file name via `-o` 23 | 24 | $ mucppo -o output.ml input.ml 25 | $ cat output.ml 26 | let () = print_endline "Hello world" 27 | -------------------------------------------------------------------------------- /mucppo.opam: -------------------------------------------------------------------------------- 1 | # This file is generated by dune, edit dune-project instead 2 | opam-version: "2.0" 3 | synopsis: "Minimal, embeddable subset of CPPO" 4 | description: """ 5 | This package provides a single-file no-dependency reimplementation of a 6 | subset of CPPO that is meant to be used in packages that use dune.""" 7 | maintainer: ["marek@xivilization.net"] 8 | license: "CC0-1.0" 9 | homepage: "https://github.com/Leonidas-from-XIV/mucppo" 10 | bug-reports: "https://github.com/Leonidas-from-XIV/mucppo/issues" 11 | depends: [ 12 | "dune" {>= "3.7"} 13 | "ocaml" {>= "4.02.3"} 14 | "odoc" {with-doc} 15 | ] 16 | build: [ 17 | ["dune" "subst"] {dev} 18 | [ 19 | "dune" 20 | "build" 21 | "-p" 22 | name 23 | "-j" 24 | jobs 25 | "@install" 26 | "@runtest" {with-test} 27 | "@doc" {with-doc} 28 | ] 29 | ] 30 | dev-repo: "git+https://github.com/Leonidas-from-XIV/mucppo.git" 31 | -------------------------------------------------------------------------------- /test/defines.t: -------------------------------------------------------------------------------- 1 | Test that defines work: 2 | 3 | $ cat > defines.ml < #define FOO 5 | > #ifdef FOO 6 | > Should print 7 | > #endif 8 | > #undef FOO 9 | > #ifdef FOO 10 | > Should not print 11 | > #endif 12 | > EOF 13 | $ mucppo < defines.ml 14 | Should print 15 | 16 | Test that `elif` works: 17 | 18 | $ cat > elif.ml < #define OTHERWISE 20 | > #ifdef UNDEFINED 21 | > Should not print 22 | > #elif defined OTHERWISE 23 | > Should print 24 | > #endif 25 | > EOF 26 | $ mucppo < elif.ml 27 | Should print 28 | 29 | $ cat > elif.ml < #define DEFINED 31 | > #ifdef DEFINED 32 | > Should print 33 | > #elif defined OTHERWISE 34 | > Should not print 35 | > #endif 36 | > EOF 37 | $ mucppo < elif.ml 38 | Should print 39 | 40 | Both values are set but it is `elif` so it should only run the first matching 41 | branch: 42 | 43 | $ cat > elif.ml < #define IF 45 | > #define ELIF 46 | > #ifdef IF 47 | > Should print 48 | > #elif defined ELIF 49 | > Should not print 50 | > #endif 51 | > EOF 52 | $ mucppo < elif.ml 53 | Should print 54 | 55 | $ cat > elif.ml < #define ELIF1 57 | > #define ELIF2 58 | > #ifdef IF 59 | > Should not print 60 | > #elif defined ELIF1 61 | > Should print 62 | > #elif defined ELIF2 63 | > Should not print 64 | > #elif defined ELIF3 65 | > Should not print 66 | > #endif 67 | > EOF 68 | $ mucppo < elif.ml 69 | Should print 70 | 71 | Nested ifdefs should work: 72 | 73 | $ cat > nested.ml < #define STRING 75 | > Nested with false condition: 76 | > #ifdef VARIANT 77 | > #ifdef STRING 78 | > Should not print 79 | > #endif 80 | > #else 81 | > VARIANT wasn't defined 82 | > #endif 83 | > Unnested: 84 | > #ifdef STRING 85 | > Should print 86 | > #endif 87 | > Nested with true condition: 88 | > #ifdef STRING 89 | > #ifdef STRING 90 | > Should print 91 | > #endif 92 | > #endif 93 | > EOF 94 | $ mucppo < nested.ml 95 | Nested with false condition: 96 | VARIANT wasn't defined 97 | Unnested: 98 | Should print 99 | Nested with true condition: 100 | Should print 101 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | µCPPO: minimal usable subset of CPPO 2 | ==================================== 3 | 4 | Code-generation via textual macros can be very useful, but it also has a number 5 | of downsides. CPPO implements a fairly complete macro language, but its 6 | downside is that it is an additional (build-time) dependency to projects. 7 | 8 | µCPPO attempts to alleviate this by being a single file dependency that is 9 | meant to be vendored into one's build. The idea is similar to the 10 | [stb](https://github.com/nothings/stb) suite of single-file header libraries. 11 | 12 | Goals 13 | ----- 14 | 15 | * No external dependencies 16 | * Single file for easy embedding 17 | * µCPPO files should remain valid CPPO files 18 | * 80% of the usecases of CPPO at 20% of the cost 19 | * Compatibility with the oldest supported compiler version that dune supports 20 | * Conservative language subset to avoid breaking in future OCaml versions 21 | 22 | Non-goals 23 | --------- 24 | 25 | * Implement the whole CPPO language 26 | * Support CPPO files unchanged 27 | * Have complete CLI parsing 28 | * Fancy error handling 29 | 30 | Usage 31 | ----- 32 | 33 | The main intended usage is within Dune, to allow for features that can't 34 | natively done with Dune itself: 35 | 36 | Create a `mucppo` folder with this `dune` file: 37 | 38 | ```scheme 39 | (executable 40 | (name mucppo)) 41 | ``` 42 | 43 | Copy `mucppo.ml` into it. 44 | 45 | Then preprocess the files that need CPPO like so: 46 | 47 | ```scheme 48 | (rule 49 | (action (with-stdout-to compat.ml 50 | (run ./mucppo/mucppo.exe compat.pp.ml)))) 51 | ``` 52 | 53 | Supported features 54 | ------------------ 55 | 56 | * OCaml version comparison: `#if OCAML_VERSION >= (major, minor, patch)` 57 | * Inclusion of other files: `#include` 58 | 59 | Unless otherwise stated, other features are not implemented. Notably, macros 60 | and macros with arguments are unlikely to ever get implemented, as they are 61 | rarely used and make codebases hard to read. 62 | 63 | License 64 | ------- 65 | 66 | [![License: CC0-1.0](https://licensebuttons.net/l/zero/1.0/80x15.png)](http://creativecommons.org/publicdomain/zero/1.0/) 67 | 68 | As µCPPO is meant to be vendored into projects, the code is public domain in as 69 | far as possible under international law. Therefore it is under 70 | [CC0](https://creativecommons.org/share-your-work/public-domain/cc0/) to 71 | facilitate relicensing and reuse. 72 | -------------------------------------------------------------------------------- /mucppo.ml: -------------------------------------------------------------------------------- 1 | (* mucppo Copyright 2023-2024 Marek Kubica 2 | * Released under CC0 license, freely available to all. 3 | * 4 | * Simple, no dependency cppo replacement to be embedded into builds. 5 | * 6 | * Contains a bare subset of cppo features to eliminate it as a dependency. 7 | * For more info check the project page at 8 | * https://github.com/Leonidas-from-XIV/mucppo 9 | *) 10 | 11 | let version_triple major minor patch = (major, minor, patch) 12 | let current_version = Scanf.sscanf Sys.ocaml_version "%u.%u.%u" version_triple 13 | let greater_or_equal (v : int * int * int) = current_version >= v 14 | 15 | module PrintingState : sig 16 | type t 17 | 18 | val empty : t 19 | val is_empty : t -> bool 20 | val flip_top : t -> t 21 | val latest_was_triggered : t -> bool 22 | val pop : t -> t 23 | val push : bool -> t -> t 24 | val should_print : t -> bool 25 | end = struct 26 | type state = { state : bool; was_true : bool } 27 | type t = state list 28 | 29 | let empty = [ { state = true; was_true = false } ] 30 | let is_empty (x : t) = x = empty 31 | 32 | let flip_top = function 33 | | [] -> failwith "Output stack empty, invalid state" 34 | | x :: xs -> { state = not x.state; was_true = true } :: xs 35 | 36 | let latest_was_triggered l = (List.hd l).was_true 37 | let pop = List.tl 38 | let push state l = { state; was_true = state } :: l 39 | 40 | let should_print = 41 | List.fold_left (fun acc { state; was_true = _ } -> state && acc) true 42 | end 43 | 44 | module Variables = struct 45 | module Map = Map.Make (String) 46 | 47 | let is_defined name = Map.mem name 48 | let define name = Map.add name () 49 | let undefine name = Map.remove name 50 | let empty = Map.empty 51 | end 52 | 53 | (* for OCaml 4.02 *) 54 | let string_equal = (=) 55 | 56 | let starts_with ~prefix s = 57 | let len = String.length prefix in 58 | String.length s >= len && string_equal (String.sub s 0 len) prefix 59 | 60 | let is_if_statement = starts_with ~prefix:"#if" 61 | let is_elif_defined_statement = starts_with ~prefix:"#elif defined" 62 | let is_include_statement = starts_with ~prefix:"#include" 63 | let is_define_statement = starts_with ~prefix:"#define" 64 | let is_undef_statement = starts_with ~prefix:"#undef" 65 | let is_ifdef = starts_with ~prefix:"#ifdef" 66 | let filename_of_include s = Scanf.sscanf s "#include %S" (fun x -> x) 67 | let variable_of_define s = Scanf.sscanf s "#define %s" (fun x -> x) 68 | let variable_of_undef s = Scanf.sscanf s "#undef %s" (fun x -> x) 69 | let variable_of_ifdef s = Scanf.sscanf s "#ifdef %s" (fun x -> x) 70 | let variable_of_elif_defined s = Scanf.sscanf s "#elif defined %s" (fun x -> x) 71 | 72 | let is_ocaml_version s = 73 | (* Sscanf.sscanf_opt exists but only since 5.0 *) 74 | match Scanf.sscanf s "#if OCAML_VERSION >= (%u, %u, %u)" version_triple with 75 | | v -> Some v 76 | | exception _ -> None 77 | 78 | module State = struct 79 | type t = { 80 | (* print state *) 81 | ps : PrintingState.t; 82 | vars : unit Variables.Map.t; 83 | } 84 | 85 | let conditional_triggered s = { s with ps = PrintingState.flip_top s.ps } 86 | let end_conditional s = { s with ps = PrintingState.pop s.ps } 87 | let triggered_before s = PrintingState.latest_was_triggered s.ps 88 | let start_conditional v s = { s with ps = PrintingState.push v s.ps } 89 | let should_output { ps; vars = _ } = PrintingState.should_print ps 90 | 91 | let don't_print s = 92 | match should_output s with 93 | | false -> s 94 | | true -> { s with ps = PrintingState.flip_top s.ps } 95 | 96 | let finished { ps; vars = _ } = PrintingState.is_empty ps 97 | let empty = { ps = PrintingState.empty; vars = Variables.empty } 98 | let define v s = { s with vars = Variables.define v s.vars } 99 | let undefine v s = { s with vars = Variables.undefine v s.vars } 100 | let is_defined v { ps = _; vars } = Variables.is_defined v vars 101 | end 102 | 103 | let output_endline oc s = 104 | output_string oc s; 105 | output_char oc '\n' 106 | 107 | let rec loop ic oc ~lineno ~filename st = 108 | match input_line ic with 109 | | line -> ( 110 | let next = loop ic oc ~lineno:(succ lineno) ~filename in 111 | match String.trim line with 112 | | "#else" -> ( 113 | match State.triggered_before st with 114 | | true -> next (State.don't_print st) 115 | | false -> next (State.conditional_triggered st)) 116 | | "#endif" -> next (State.end_conditional st) 117 | | trimmed_line when is_define_statement trimmed_line -> 118 | let var = variable_of_define trimmed_line in 119 | let st = State.define var st in 120 | next st 121 | | trimmed_line when is_undef_statement trimmed_line -> 122 | let var = variable_of_undef trimmed_line in 123 | let st = State.undefine var st in 124 | next st 125 | | trimmed_line when is_include_statement trimmed_line -> 126 | let filename = filename_of_include trimmed_line in 127 | let included_ic = open_in filename in 128 | loop included_ic oc ~lineno:1 ~filename st; 129 | next st 130 | | trimmed_line when is_ifdef trimmed_line -> 131 | let var = variable_of_ifdef trimmed_line in 132 | let is_defined = State.is_defined var st in 133 | let st = State.start_conditional is_defined st in 134 | next st 135 | | trimmed_line when is_if_statement trimmed_line -> ( 136 | match is_ocaml_version line with 137 | | None -> 138 | failwith 139 | (Printf.sprintf "Parsing #if in file %s line %d failed, exiting" 140 | filename lineno) 141 | | Some (major, minor, patch) -> 142 | next 143 | (State.start_conditional 144 | (greater_or_equal (major, minor, patch)) 145 | st)) 146 | | trimmed_line when is_elif_defined_statement trimmed_line -> ( 147 | match State.triggered_before st with 148 | | true -> next (State.don't_print st) 149 | | false -> ( 150 | let var = variable_of_elif_defined trimmed_line in 151 | match State.is_defined var st with 152 | | true -> next (State.conditional_triggered st) 153 | | false -> next st)) 154 | | _trimmed_line -> 155 | if State.should_output st then output_endline oc line; 156 | next st) 157 | | exception End_of_file -> 158 | if not (State.finished st) then 159 | failwith "Output stack messed up, missing #endif?" 160 | 161 | let () = 162 | let output_file = ref None in 163 | let input_file = ref None in 164 | let speclist = 165 | [ 166 | ( "-o", 167 | Arg.String (fun filename -> output_file := Some filename), 168 | "Set output file name" ); 169 | ] 170 | in 171 | let anonymous filename = input_file := Some filename in 172 | let usage = "mucppo -o " in 173 | Arg.parse speclist anonymous usage; 174 | let ic, filename = 175 | match !input_file with 176 | | Some filename -> (open_in filename, filename) 177 | | None -> (stdin, "") 178 | in 179 | let oc = 180 | match !output_file with 181 | | Some filename -> open_out filename 182 | | None -> stdout 183 | in 184 | loop ic oc ~lineno:1 ~filename State.empty; 185 | close_in ic; 186 | close_out oc 187 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Creative Commons Legal Code 2 | 3 | CC0 1.0 Universal 4 | 5 | CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE 6 | LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN 7 | ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS 8 | INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES 9 | REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS 10 | PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM 11 | THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED 12 | HEREUNDER. 13 | 14 | Statement of Purpose 15 | 16 | The laws of most jurisdictions throughout the world automatically confer 17 | exclusive Copyright and Related Rights (defined below) upon the creator 18 | and subsequent owner(s) (each and all, an "owner") of an original work of 19 | authorship and/or a database (each, a "Work"). 20 | 21 | Certain owners wish to permanently relinquish those rights to a Work for 22 | the purpose of contributing to a commons of creative, cultural and 23 | scientific works ("Commons") that the public can reliably and without fear 24 | of later claims of infringement build upon, modify, incorporate in other 25 | works, reuse and redistribute as freely as possible in any form whatsoever 26 | and for any purposes, including without limitation commercial purposes. 27 | These owners may contribute to the Commons to promote the ideal of a free 28 | culture and the further production of creative, cultural and scientific 29 | works, or to gain reputation or greater distribution for their Work in 30 | part through the use and efforts of others. 31 | 32 | For these and/or other purposes and motivations, and without any 33 | expectation of additional consideration or compensation, the person 34 | associating CC0 with a Work (the "Affirmer"), to the extent that he or she 35 | is an owner of Copyright and Related Rights in the Work, voluntarily 36 | elects to apply CC0 to the Work and publicly distribute the Work under its 37 | terms, with knowledge of his or her Copyright and Related Rights in the 38 | Work and the meaning and intended legal effect of CC0 on those rights. 39 | 40 | 1. Copyright and Related Rights. A Work made available under CC0 may be 41 | protected by copyright and related or neighboring rights ("Copyright and 42 | Related Rights"). Copyright and Related Rights include, but are not 43 | limited to, the following: 44 | 45 | i. the right to reproduce, adapt, distribute, perform, display, 46 | communicate, and translate a Work; 47 | ii. moral rights retained by the original author(s) and/or performer(s); 48 | iii. publicity and privacy rights pertaining to a person's image or 49 | likeness depicted in a Work; 50 | iv. rights protecting against unfair competition in regards to a Work, 51 | subject to the limitations in paragraph 4(a), below; 52 | v. rights protecting the extraction, dissemination, use and reuse of data 53 | in a Work; 54 | vi. database rights (such as those arising under Directive 96/9/EC of the 55 | European Parliament and of the Council of 11 March 1996 on the legal 56 | protection of databases, and under any national implementation 57 | thereof, including any amended or successor version of such 58 | directive); and 59 | vii. other similar, equivalent or corresponding rights throughout the 60 | world based on applicable law or treaty, and any national 61 | implementations thereof. 62 | 63 | 2. Waiver. To the greatest extent permitted by, but not in contravention 64 | of, applicable law, Affirmer hereby overtly, fully, permanently, 65 | irrevocably and unconditionally waives, abandons, and surrenders all of 66 | Affirmer's Copyright and Related Rights and associated claims and causes 67 | of action, whether now known or unknown (including existing as well as 68 | future claims and causes of action), in the Work (i) in all territories 69 | worldwide, (ii) for the maximum duration provided by applicable law or 70 | treaty (including future time extensions), (iii) in any current or future 71 | medium and for any number of copies, and (iv) for any purpose whatsoever, 72 | including without limitation commercial, advertising or promotional 73 | purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each 74 | member of the public at large and to the detriment of Affirmer's heirs and 75 | successors, fully intending that such Waiver shall not be subject to 76 | revocation, rescission, cancellation, termination, or any other legal or 77 | equitable action to disrupt the quiet enjoyment of the Work by the public 78 | as contemplated by Affirmer's express Statement of Purpose. 79 | 80 | 3. Public License Fallback. Should any part of the Waiver for any reason 81 | be judged legally invalid or ineffective under applicable law, then the 82 | Waiver shall be preserved to the maximum extent permitted taking into 83 | account Affirmer's express Statement of Purpose. In addition, to the 84 | extent the Waiver is so judged Affirmer hereby grants to each affected 85 | person a royalty-free, non transferable, non sublicensable, non exclusive, 86 | irrevocable and unconditional license to exercise Affirmer's Copyright and 87 | Related Rights in the Work (i) in all territories worldwide, (ii) for the 88 | maximum duration provided by applicable law or treaty (including future 89 | time extensions), (iii) in any current or future medium and for any number 90 | of copies, and (iv) for any purpose whatsoever, including without 91 | limitation commercial, advertising or promotional purposes (the 92 | "License"). The License shall be deemed effective as of the date CC0 was 93 | applied by Affirmer to the Work. Should any part of the License for any 94 | reason be judged legally invalid or ineffective under applicable law, such 95 | partial invalidity or ineffectiveness shall not invalidate the remainder 96 | of the License, and in such case Affirmer hereby affirms that he or she 97 | will not (i) exercise any of his or her remaining Copyright and Related 98 | Rights in the Work or (ii) assert any associated claims and causes of 99 | action with respect to the Work, in either case contrary to Affirmer's 100 | express Statement of Purpose. 101 | 102 | 4. Limitations and Disclaimers. 103 | 104 | a. No trademark or patent rights held by Affirmer are waived, abandoned, 105 | surrendered, licensed or otherwise affected by this document. 106 | b. Affirmer offers the Work as-is and makes no representations or 107 | warranties of any kind concerning the Work, express, implied, 108 | statutory or otherwise, including without limitation warranties of 109 | title, merchantability, fitness for a particular purpose, non 110 | infringement, or the absence of latent or other defects, accuracy, or 111 | the present or absence of errors, whether or not discoverable, all to 112 | the greatest extent permissible under applicable law. 113 | c. Affirmer disclaims responsibility for clearing rights of other persons 114 | that may apply to the Work or any use thereof, including without 115 | limitation any person's Copyright and Related Rights in the Work. 116 | Further, Affirmer disclaims responsibility for obtaining any necessary 117 | consents, permissions or other rights required for any use of the 118 | Work. 119 | d. Affirmer understands and acknowledges that Creative Commons is not a 120 | party to this document and has no duty or obligation with respect to 121 | this CC0 or use of the Work. 122 | --------------------------------------------------------------------------------