Skip to content

Pass a typed compile request from the build system to the compiler, and validate user compiler flags #9303

Description

@cknitt

Problem

The build system and the compiler talk through argv strings, even though the build system runs the compiler in the same process.

  • rewatch-ocaml/compiler_args.ml builds bsc's argv as a string list. In-process builds hand that list to Rescript_compiler_driver.run_request, which parses it again with the flag table in command_line_flags.
  • Nothing checks this contract at compile time. A misspelled or misordered flag only fails when a module is compiled.
  • User flags are not checked at all. Config_decode.compiler_flags splits the compiler-flags entries of rescript.json on spaces and passes them through. An unknown flag or a bad warning spec makes every module of the package fail with the same error, instead of one error pointing at rescript.json.
  • A user can pass flags that the build system sets itself (-bs-ns, -o, -I, -bs-package-output, …). They silently conflict with the build system's own.
  • Values go through string quoting, which is fragile for Windows paths.

Proposal

Give a compile a typed record, with argv as its serialized form:

type compile_request = {
  source: string;
  namespace: string option;
  alias_maps: string list;
  include_dirs: string list;
  package_name: string;
  package_outputs: package_output list;
  read_cmi: bool;
  runtime_path: string;
  project_root: string;
  warnings: Warnings.spec;
  open_modules: string list;
  ppx: string list;
  (* … one field per flag the build system or users set *)
}

val of_argv : string list -> (compile_request, error) result
val to_argv : compile_request -> string list
val apply : compile_request -> unit  (* sets the request-local compiler state *)
  • One flag table. command_line_flags stays the only list of flags. Each entry updates a field of the record instead of a global ref.
  • In-process builds build the record and call apply. Nothing is printed or parsed.
  • External bsc keeps its command line: of_argv, then apply. Both paths run the same apply.
  • rescript compiler-args and --verbose print to_argv, so a printed command still reproduces the compile.
  • User flags from compiler-flags are parsed with of_argv when the config is loaded. Unknown flags, bad values (warning specs go through the compiler's own parser) and flags reserved for the build system become config errors with the field's location.
  • File-level @@config({flags: [...]}) uses the same of_argv, applied on top of the package's request for that file.
  • Parse requests (parser_arguments) get the same treatment with their own smaller record.

Values that are text by nature, such as a ppx command or a module name to open, stay strings, but as typed fields, not free argv. bsc's debug-only flags can stay argv-only.

Steps

  1. Validate user flags at config load by running the existing flag parser on them in a fresh request state. Reject flags the build system owns. This is a small change that already gives located errors.
  2. Introduce the record, with of_argv/to_argv/apply and a round-trip test, of_argv (to_argv r) = Ok r.
  3. Pass the record in-process from rewatch-ocaml, and generate argv only for compiler-args and --verbose.

Related

#9301 adds -bs-open-alias-map next to -bs-ns. With this change, both become fields (namespace, alias_maps) instead of flags whose effects overlap.

Activity

  1. added theissue type on Nov 14, 2026
  2. cknitt commented on Nov 14, 2026

    @cknitt
    MemberAuthor

    Design for the typed compile request

    Kinds of flags

    Each entry of bsc's flag table (command_line_flags) gets a kind. Where a flag may appear depends on its kind:

    Kind Flags compiler-flags @@config
    Build system -I, -o, -bs-read-cmi, -bs-ns, -bs-package-name, -bs-package-output, -bs-project-root, -runtime-path, -bs-ast, -no-alias-deps, -absname, -color, -bs-gentype-* error error
    Config-owned -bs-jsx, -bs-jsx-module, -bs-jsx-preserve, -bs-source-map*, -enable-experimental, -ppx, -bs-gentype warning that points at the rescript.json field allowed
    User -open, -w, -warn-error, -bs-g, -bs-no-version-header, -bs-(no-)cross-module-opt, -bs-no-check-div-by-zero, -bs-noassertfalse, -noassert, -nostdlib, -nopervasives, -make-runtime, -bs-no-bin-annot, -(no-)keep-locs, -short-paths, -bs-no-builtin-ppx, -bs-cmi, -bs-cmj, -check-lam, -debug-ir allowed allowed
    Debug -dparsetree, -dtypedtree, -drawlambda, -draw-coercions, -dsource, -bs-loc, -only-parse, -bs-syntax-only, -bs-cmi-only, -ignore-parse-errors, -editor-mode, -as-pp, -bs-test-ast-conversion, -verbose error allowed
    Command -e, -v, -version, -warn-help, -format, -reprint-source, -help error error

    @@config keeps config-owned and debug flags because tests and projects use them per file, such as -bs-jsx and -bs-test-ast-conversion.

    The record

    A module Compile_request holds every flag except the commands. Lists keep command-line order (-w, -open, -I); for a pair such as -bs-cross-module-opt and -bs-no-cross-module-opt, the last one wins. Related flags are grouped: jsx, source_map, gentype, codegen, stdlib, and debug (bools).

    val of_argv : ?base:t -> allowed:(kind -> bool) -> string list -> (t * command option * string list, error) result
    val to_argv : t -> string list
    val apply : t -> unit  (* the only writer of the request-local Clflags/Js_config/Gentype_config fields *)
    • Each table entry updates the record (t -> t or t -> string -> t) instead of global state. Values are checked when parsed: jsx version, source-map mode, gentype module, and warning specs (through the warning parser on backed-up state).
    • A round-trip test runs every table entry through to_argv and of_argv.
    • Clflags and Js_config keep their request-local fields; apply fills them.

    Who uses what

    • bsc's command line: of_argv with every kind allowed, then the command or apply. No behaviour change.
    • rewatch: builds the record directly and passes it to the in-process compiler. compiler-flags are parsed into it with the rules above.
    • compiler-args, --verbose, compiler log and compiler-info.json: print to_argv. The editor's JSON format is unchanged.
    • @@config: of_argv ~base:current with the @@config rules, then apply.

    Pull requests

    1. Check compiler-flags when a build starts #9304: checks compiler-flags at build start (as it is now).
    2. PR A: the record, the kinds, of_argv/to_argv/apply and the round-trip test, with bsc's command line using them. No behaviour change.
    3. PR B: in-process records from rewatch, compiler-flags parsed into the record (replaces Check compiler-flags when a build starts #9304's check, adds the warning), and the @@config rules.
    4. Keep a namespaced package's dev modules out of its namespace #9301 rebased: -bs-open-alias-map becomes the alias_maps field.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions