Skip to content

[RFC] Switch to JSON #3

Description

@digama0

The concrete syntax of the export format is troublesome to parse, being a homegrown textual format with a heavy reliance on newline separators and with lots of "clever" sequence encodings. Moreover, the encoding of names is unquoted, which is just plain wrong in the presence of names with escaped characters, because these names can include arbitrary characters, including newlines, keywords and everything else - a classic SQL injection attack.

I propose we drop this ad hoc encoding entirely and switch to a JSON-based format. This is much easier to get right, and libraries for doing the parsing are numerous (but it's also feasible to write the parser directly).

Activity

  1. ammkrn commented on Jan 1, 2024

    @ammkrn
    Contributor

    Fwiw I'm not opposed to the idea; while the current format is really easy to write a parser for, JSON would certainly be more accessible for people/projects that don't want to write a parser in the first place. I haven't looked deeply into the name escaping issue yet, but I'll take your word for it that it's a potential source of problems.

    I'm interested to see what you come up with.

  2. adomasbaliuka commented on Aug 23, 2024

    @adomasbaliuka

    JSON has a quite complicated spec with lots of extensions which aren't implemented exactly the same (or strictly according to the spec) by different libraries.

    Perhaps something slightly more limited and more standardized would be better? (Ideally one would also use a formally verified parser, which after searching for a bit I'm still not quite sure about the status of for JSON)

    Perhaps a subset of JSON would work? (Which one would specify separately for the purpose of lean4export.)

  3. digama0 commented on Aug 24, 2024

    @digama0
    CollaboratorAuthor

    I'm not sure what you are referring to, JSON is an exceedingly simple format. The only part I'm aware of tricky bits in json is for representation of large integers and floating point, but I don't think these are likely to be issues since we don't need floats at all and it is unlikely for numbers to get that large except when representing lean bignums, and string encoding these solves the problem completely.

  4. adomasbaliuka commented on Aug 24, 2024

    @adomasbaliuka

    All I'm saying is that there isn't an ironclad standard for how to parse JSON and many (also "standard") parser implementations differ. Some examples: here.

    I thought maybe it's important for this export format to have an ironclad standard. Am I wrong? I guess it's also reasonable to say something like "let the parser fail on exported valid lean proofs in extremely rare cases (e.g. if they contain too big numbers), let it crash on malformed input, let it output whatever it wants, the kernel will check everything in the end and that's all that matters".

  5. digama0 commented on Aug 24, 2024

    @digama0
    CollaboratorAuthor

    All I'm saying is that there isn't an ironclad standard for how to parse JSON and many (also "standard") parser implementations differ. Some examples: here.

    This is not true. What that page shows is that there exist multiple documents that specify JSON, and parsers sometimes accept more or less than the spec due to implementation limits or "extensions". I don't see why any of this matters, I think you should be more specific.

    Regarding having an ironclad standard, I'm not seeing anything here which prevents having such. But this is a proof format, which means that it actually doesn't matter if there are edge cases which are interpreted oddly, because proof checkers are allowed to spuriously fail for implementation limits reasons or even just not liking the shape of the proof. That's a quality-of-implementation issue, not a correctness issue.

  6. daniel-levin commented on Feb 15, 2025

    @daniel-levin

    I thought maybe it's important for this export format to have an ironclad standard. Am I wrong?

    No, you are not wrong. I don't think anyone wants to allow spurious errors to be introduced by a poorly designed serialization format.

    With this shared goal in mind, I maintain that imperfect serialization formats, such as JSON, are fine for all pragmatic purposes, provided their shortcomings are dealt with by the serializer - in this case lean4export.

    Moreover, "ironclad" serialization formats are rarely truly ironclad. Even the most strictly specified, such as BER/DER - widely used in public key cryptography systems - are replete with deliberately non-conformant implementations. Example: RustCrypto/formats#779.

    Finally, the advantages of an export format that can be understood by practically every programming language far outweigh the downsides. Sure, using JSON makes ambiguity and/or invalidity possible, though unlikely. The principal advantage of JSON (or TOML, my preferred format) is ease of implementation. I am optimistic that making it easy to get started will result in more implementations, which will result in more scrutiny of the proof terms constructed by Lean. A more diverse set of kernels will necessarily result in more scrutiny of the serializer itself. By this logic, JSON is a reasonable candidate.

  7. ammkrn commented on Oct 26, 2025

    @ammkrn
    Contributor

    The issue of escaped identifiers is now a live issue, I think due to these library_note things, e.g. this produces a NS export line with a space in the name suffix.

  8. ammkrn commented on Nov 28, 2025

    @ammkrn
    Contributor

    I'm going to be moving forward with this since it's currently an issue in mathlib as mentioned previously; here's an initial pass for what the object layout might look like as markdown and as an (llm derived) json spec. I'm assuming the norm will be ndjson.

    This follows Lean's rules for JSON serialization of inductives (the ones used by the deriving handlers) with the exception that one layer of val bureaucracy for the ConstantVal items is removed.

  9. digama0 commented on Dec 1, 2025

    @digama0
    CollaboratorAuthor

    LGTM

  10. Gravifer commented on Dec 1, 2025

    @Gravifer

    What about migrating to something like EDN? I don't think you can bypass parser writing that way (much smaller userbase), but it does have better type semantics; should be less ambiguous

  11. ammkrn commented on Dec 9, 2025

    @ammkrn
    Contributor

    What about migrating to something like EDN? I don't think you can bypass parser writing that way (much smaller userbase), but it does have better type semantics; should be less ambiguous

    My assumption is that people will want to stick with JSON unless you're aware of any existing ambiguity concerns that might be an issue (if so, please share them). On the bright side the exporter is very small, so it should be easy to fork in order to support different formats.

  12. nomeata commented on Dec 24, 2025

    @nomeata
    Contributor

    How concrete is this proposal here? Do we already know how much larger exports of, say, mathlib get, and whether parsing is slower or faster?

  13. ammkrn commented on Dec 24, 2025

    @ammkrn
    Contributor

    How concrete is this proposal here? Do we already know how much larger exports of, say, mathlib get, and whether parsing is slower or faster?

    At full verbosity using this the output is basically twice the size. I'm not opposed to having a "compact" flag for using shortened property/field names. I don't speak for Lean or the FRO, but since this is the semi-official export tool, I would assume the desire is for something legible that's not going to break, at least as the default output.

    How concrete is this proposal here?

    I think we're all open to suggestions if you have some.

  14. nomeata commented on Dec 24, 2025

    @nomeata
    Contributor

    My gut feeling likes the old format, but for no good reason, and factor 2 seems reasonable, so no complaints from my side.

    The main reason I'm asking is if I'd build tools that consume the exported format, if I should simply ignore the old format because it's going to be replaced real soon now, or if this is more a mid-term project here and I shouldn't wait for it.

  15. ammkrn commented on Dec 24, 2025

    @ammkrn
    Contributor

    My gut feeling likes the old format, but for no good reason, and factor 2 seems reasonable, so no complaints from my side.

    The main reason I'm asking is if I'd build tools that consume the exported format, if I should simply ignore the old format because it's going to be replaced real soon now, or if this is more a mid-term project here and I shouldn't wait for it.

    You should probably ignore the old (current) format, because it's incapable of exporting mathlib releases after the introduction of library_note2, and there are no plans to patch it. In addition to the identifier/string issue, it's not really capable of exporting mdata expressions which at least one user has credibly expressed interest in doing.

    The fork I linked in my previous message should more or less work right now. I need to finish writing a new parser, probably make some changes to the exporter in response to whatever is going to come up in testing, and then I'll file a PR.

  16. 9 remaining items

  17. ammkrn commented on Jan 10, 2026

    @ammkrn
    Contributor

    Two of other things I noticed:

    * why do Level.max/imax take their arguments as an array as opposed to everything else?
    
    * I do not understand how to recover an mdata node with the currently used serialization mechanism. The resulting JSON seems entirely unrecoverable to the original format to me in the general case.
    

    Perhaps I should have disclosed earlier, but I have no idea what the proposed use cases are for mdata in the export file and didn't think very hard about what to do with it. I have no objection to changing the layout and have no input on what a better format might be, other than maybe using the same integer based referencing for the name part of the KVMap keys.

    max/imax use an array because they're the only elements that have anonymous constructor arguments and I was (at least initially) trying to stick to what Lean was doing.

  18. digama0 commented on Jan 11, 2026

    @digama0
    CollaboratorAuthor

    I think it would be best to keep mdata because it changes the expr hash. There was recently a change to remove nonDep from export to fix the same issue there but I don't think stripping mdata from oleans is reasonable, so I think the checker should just support mdata and do nothing with it (treat it like an identity function which eagerly unfolds in whnf).

  19. nomeata commented on Jan 11, 2026

    @nomeata
    Contributor

    I think the checker should just support mdata and do nothing with it (treat it like an identity function which eagerly unfolds in whnf).

    right, which is also what the official kernel does.

    OTOH, it seems a bit overkill to define a stable format for the whole algebra of mdatas, including KVMap and Syntax(!), when for the envisioned applications of the export format we have no use for this. This also means that the format will not be stable across version of lean that only change the MData. And, at least ideally, the expression hash is an implementation detail that shouldn’t have relevance for an external stable proof representation.

  20. ammkrn commented on Jan 11, 2026

    @ammkrn
    Contributor

    I can deal with mdata potentially being included in the default output, but if that's going to be the case I would really prefer to retain a flag to get an export with no mdata. FWIW I do not intend to support mdata in the rust checker.

  21. nomeata commented on Jan 12, 2026

    @nomeata
    Contributor

    @ammkrn, from reading your rust parser it seems that you assume that the indices in the JSON file are used in a strictly increasing contiguous fashion. Is that an invariant you wanted to be part of the format, or was that just an implemenation shortcut in the parser for now?
    (Requiring them to be in increasing order may simplify some export-to-export-format transformation tools, e.g. tools that filter the environment.)

  22. ammkrn commented on Jan 12, 2026

    @ammkrn
    Contributor

    Off the top of my head I'm pretty sure I can make changes to accommodate indices that appear out of order, but non-contiguous might be tougher. The current setup arises pretty naturally out of the way the exporter currently works and is a nice sanity check for consumers, but maybe you had some kind of parallelism in mind?

    I do have one more change I'd like to push that tries to ensure the elements of an inductive declaration (inductive, constructors, recursors) are exported together.

    It may or may not also be worth it to have something that tries to export the relevant Nat and String declarations on the first appearance of a nat or string literal. It's technically possible to export an Expr.lit _ before the appropriate types are declared; users may expect that in any lean module using a literal (in a project that also has the requisite Nat/String declarations somewhere) that they're always going to get the "fast" kernel behavior, but that's not necessarily the case.

  23. nomeata commented on Jan 12, 2026

    @nomeata
    Contributor

    I'm hesitant to make such rules part of the export format requirements. It seems that checkers (at least lean4checker and lean4lean) already do a topological sort after parsing.

    I think it's reasonable to have a literal without any further operations in an export, at least if you don't do any operations with it.

    This is probably bike shedding dangerous corner, given that it's mostly relevant for artificially short exports.

  24. ammkrn commented on Jan 12, 2026

    @ammkrn
    Contributor

    I agree that it's reasonable for an exported environment to have a literal without anything else, the idea is that if environment to be exported has the relevant String/Nat declarations AND they are able to be exported prior to any Expr.lit items, it might be advantageous for the exporter to do that.

    I'm hesitant to make such rules part of the export format requirements. It seems that checkers (at least lean4checker and lean4lean) already do a topological sort after parsing.

    To the extent this is not about the literals, in the interest of helping any consumer software stay as small as possible, I think when we have to answer the question of "should the exporter be responsible for this or the type checker", the default answer should be the exporter. Even if the type checker has to do something to ensure an invariant/order/whatever has been done properly, it's generally going to be smaller than if it had to do the thing itself.

    I can change the export format for inductives to reflect the fact that they should be exported together, I think something like this is probably better anyway, especially for complex mutual/nested declarations:

    {
        "inductive": {
            "inductInfos": [..]
            "ctorInfos": [..]
            "recInfos": [..]
        }
    }
    
  25. nomeata commented on Jan 12, 2026

    @nomeata
    Contributor

    If we are going that direction then one could argue that really the entries of the export should be modeled after Lean.Declaration, and really reflect what’s being passed to the kernel, and not what comes out of it. For example, there should be no recInfo, as that’s something that the kernel produces based on the inductDecl.

  26. nomeata commented on Jan 12, 2026

    @nomeata
    Contributor

    But that’s turning into a much bigger change than just the replacement of the textual format

  27. nomeata commented on Jan 15, 2026

    @nomeata
    Contributor

    JFTR, there was some discussion over at #10 (comment) where @ammkrn made good arguments why the slighly redundant format is better. Instead of going from List Lena.ConstantInfo to List Lean.Declaration all the way, his proposed format is a combination of the two that combines the best of both worlds, which is convenient for different consumers.

  28. nomeata commented on Jan 16, 2026

    @nomeata
    Contributor

    Looks like we are converging?

    What about versioning? The proposed metadata has fields for tool versions, but should we add a field for the format version itself, so that future changes to the format can get a version, and parsers can implement backwards compatibility?

  29. nomeata commented on Jan 16, 2026

    @nomeata
    Contributor

    @hargoniX, any final suggestions for this iteration of the format? Will you merge?

  30. hargoniX commented on Jan 16, 2026

    @hargoniX
    Member

    I think it is fine yes.

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

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions