Skip to content

Latest commit

 

History

103 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

DotCL C# Lisp Package Generator

Overview

dotcl-packagegen is a standalone CLI tool (packaged as a dotnet tool) that generates DotCL Common Lisp packages/bindings for arbitrary .NET assemblies. It is a hybrid C#/Common Lisp codebase: C# does .NET reflection and hosts the DotCL Lisp runtime; Lisp does the actual code generation via string templating.

For details on the C# capabilities exposed in the generated Lisp packages, please see FEATURES.

How The Generator Works

A single invocation generates everything for one or more assemblies in one pass:

  • Metadata reflection (pure C#, no DotCL needed): for each --assembly, AssemblyToLispy.cs reflects over the .NET assembly (plus its sidecar .xml doc file, if present) and emits a single Lisp-reader-compatible s-expression list — one plist per public type — to a <AssemblyName>.lispy.metadata file in --out-dir. See doc/assembly-to-lispy.md for the complete, canonical schema of this format (keys, flags, parameter plists, documentation plists, default-value literal formatting, etc.)

  • Package generation (boots DotCL): once every assembly's metadata has been reflected, the metadata plus the requested classes are handed to the code generator (the apg-*.lisp modules; see CLAUDE.md's architecture map or FILES.md for the per-file breakdown) (run-assembly-package-generator-batchgenerate-assembly-packages-batchgenerate-class-file), which emits a .lisp file per requested class defining a package with idiomatic Lisp wrapper functions for that C# type's constructors, methods, properties, and fields. The same invocation also writes packages.lisp (every generated package's cl:defpackage form, including a shared csharp-assembly-utils support package), csharp-assembly-utils.lisp (a small runtime-support condition type generated code depends on), and csharp-assembly-packages.asd (an ASDF system tying the whole batch together), so the output directory is self-contained and loadable with a single (asdf:load-system "csharp-assembly-packages"). All files generated by one invocation share a single creation timestamp.

An --assembly with no --class options is valid — it emits only that assembly's metadata file, generating no packages, e.g. to inspect an assembly's reflected metadata without generating any Lisp packages from it.

The metadata is a single Lisp s-expression.

Origin

This package was split out of my DotCL Dungeon Slime MonoGame proof of concept.

CLI Usage

dotcl-packagegen --out-dir ./cspackages \
    --assembly path/to/Some.Assembly.dll \
      --class Some.Namespace.Type1 --constant-properties "*" \
      --class Some.Namespace.Type2 --export-parents \
    --assembly path/to/Some.Other.Assembly.dll \
      --class Some.Other.Namespace.Type3

--class attaches to the most recently given --assembly; --constant-properties attaches to the most recently given --class. --assembly may be repeated to process several assemblies in one invocation, and a --assembly with no --class options is valid (metadata-only).

--all-classes <namespace>/--all-classes-recursive <namespace> behave like --class but name a whole C# namespace instead of one class: they expand, against the assembly's own already-reflected metadata, into every public type whose namespace is exactly <namespace> (--all-classes) or <namespace> or any sub-namespace (--all-classes-recursive, never a bare string prefix — Gum.Form does not match Gum.Forms.X), each carrying that group's own per-class flags/--constant-properties, exactly like an explicit --class:

dotcl-packagegen --out-dir ./cspackages \
    --assembly Some.Assembly.dll \
      --all-classes 'Some.Namespace' --defgeneric \
      --all-classes-recursive 'Some.Other.Namespace'

A namespace matching zero types is an error (like an unresolvable --class name), unless --skip-missing, in which case it's dropped with a warning instead. An explicit --class always wins over an overlapping namespace expansion, regardless of which comes first on the command line. Namespace expansion is scoped to its own --assembly group, like every other per-class option.

--all-classes/--all-classes-recursive do not support wildcards/globs (System.*o*) — only an exact namespace or its recursive sub-namespaces.

A real invocation for a project with more than a handful of classes gets long fast; put it in a response file instead and pass --options-file <path>:

dotcl-packagegen --out-dir ./cspackages --options-file dotcl-packagegen-options.txt

--options-file's tokens are spliced into the argument list at the exact position --options-file appears (order matters — several flags above are position-sensitive by design), so it can be freely mixed with regular command-line arguments, or split across several files each covering one --assembly group. Format: one or more whitespace-separated arguments per line; # at the start of a token begins a line comment; a double-quoted token may contain whitespace (only \" and \\ are recognized escapes) — unlike the shell, a generic class name's backtick (e.g. System.ValueTuple\2) needs no quoting or escaping at all in a response file. Not recursive: a response file containing its own --options-fileis an error. Seetest-options.txt.in(the templatemake test`'s own smoke-test invocation is generated from) for a real example.

--constant-properties (comma/semicolon-separated names, or "*" for all) forces static read-only properties to be memoized — computed once, on first use, then cached for the life of the program — instead of re-evaluated on every reference; safe only when the property genuinely never changes at runtime (e.g. Vector2.Zero), since reflection alone can't tell constants from properties that vary.

Nested C# types (a class/struct/enum declared inside another type) are addressed by their CIL name, which separates nesting levels with + rather than . — e.g. --class Microsoft.Xna.Framework.Graphics.SpriteFont+Glyph for the Glyph type nested inside SpriteFont. The generated Lisp package/file name flattens the + the same way it flattens namespace dots, so that example generates microsoft-xna-framework-graphics-sprite-font-glyph, not a name containing a literal +.

Assembly files are validated to exist, and requested classes are validated to exist in their assembly's metadata, before any output file is written; any error is reported in red to standard error. --version/--help and --test boot the DotCL host (DotclHost.Initialize()); the metadata-reflection portion of a --out-dir invocation intentionally does not, since it's pure reflection and runs before DotCL boots.

Parents and Interfaces

A class's package normally contains only the members it declares itself, not anything inherited. Per-class flags (attach to the most recently given --class, like --constant-properties) opt a class into also generating packages for, and re-exporting non-conflicting members from, its ancestors:

  • --export-parents — also generate a package for, and re-export from, every super-class (excluding System.Object unless --export-object is also given).
  • --export-interfaces — also generate a package for, and re-export from, every interface the class implements.
  • --export-object — when combined with --export-parents, also include System.Object.

Each of these has a --no- counterpart (--no-export-parents, --no-export-interfaces, --no-export-object) that turns it back off for just that one class — useful for opting a single class out when a --export-all-* sticky default (below) is otherwise on.

Sticky defaults (--export-all-parents/--no-export-all-parents, and the -interfaces/ -object equivalents) change the default for the current and every subsequent --class, in command-line order; a class's own --export-*/--no-export-* flags always override the sticky default for that one class only.

A re-exported member is skipped (with a comment, not silently dropped) when the class already declares a member of that name itself (the class's own wins), or when more than one ancestor exports the same name (ambiguous — a future version may add a renamed re-export for this case; for now it's a comment only). By default, an ancestor that cannot be found in any provided assembly's metadata is a hard error before anything is generated; pass --skip-missing to instead warn and drop it (--no-skip-missing restores the default). See doc/parents-and-interfaces-plan.md for the full design.

Building & Testing

A Makefile is provided with the following targets:

  • make build — Builds the project (dotnet build dotcl-packagegen.csproj -c Debug), compiling both the C# host and the DotCL Common Lisp sources into bin/Debug/net10.0/.

  • make test — Builds the project (via build), then runs the built executable in --test mode. This runs the generator's own Lisp unit tests (package-generator-tests-*.lisp, registered via generator-tests.lisp, including the generated System.TimeSpan operator-overload checks) followed by the AssemblyToLispy metadata test suite against System.Runtime.dll, System.Console.dll, the synthetic AssemblyToLispyTestTarget.dll, and DotCL.Runtime.dll. It then generates a real batch of packages end-to-end into cspackages-test/, verifies balanced parentheses with check_parens.py, and finally read-checks the same output with --read-check (a real Lisp reader read-back, catching invalid-token bugs paren-balance checking alone cannot see).

  • make test-runtime — The runtime exercise suite (RuntimeExerciseTest/, see doc/plan-fable-detail-02.md): generates real packages against AssemblyToLispyTestTarget fixture classes, BCL classes (System.TimeSpan/DateTime/Text.StringBuilder), and — from the same real-world libraries dotcl-dungeonslime consumes — MonoGame (Vector2, Color, Point, Rectangle, MathHelper, GameTime, Input.Keys) and Gum (DimensionUnitType, KeyCombo, TextRuntime) into RuntimeExerciseTest/gen/, then a sibling C# project cross-compiles and actually calls the generated wrapper functions against live .NET objects — the structural fix for the v48-v50 escape class (omitted-optional-passed-as-nil, Master Wrapper dispatch ordering, Nullable<T> guards), all runtime-dispatch bugs invisible to make test's string-level (paren-balance/read-back) checks above. The MonoGame/Gum assemblies are staged from the local NuGet cache (versions pinned in the Makefile, matching RuntimeExerciseTest.csproj's PackageReferences). Run it before any release, or after touching overload dispatch/codegen.

  • make package — Builds Release binaries for every configured RuntimeIdentifier (linux-x64, linux-arm64, win-x64, osx-x64, osx-arm64, any) and produces the installable NuGet package(s) (dotnet pack -c Release -o nupkg) in the nupkg/ directory: one package per RID plus a top-level meta-package that dispatches to them.

  • make deploy — Depends on package, then installs (or reinstalls, if already present) dotcl-packagegen as a global dotnet tool from the package(s) just built (dotnet tool install --global --add-source nupkg dotcl-packagegen), making the dotcl-packagegen command available on PATH from any directory.

  • make clean — Runs dotnet clean and removes the bin/, obj/, AssemblyToLispyTestTarget/bin/, AssemblyToLispyTestTarget/obj/, RuntimeExerciseTest/bin/, RuntimeExerciseTest/obj/, RuntimeExerciseTest/gen/, and nupkg/ directories.

Typical workflow: make build test while developing, make deploy to install a local build as the system-wide dotcl-packagegen command.

Design Notes

Please see generator-design-notes for some internal details on how the generator works.

Future Directions

Please see PLAN for some of the planned future directions, as well as suggested-improvements.

AI Agents

Please see GEMINI (also linked to AGENTS) and CLAUDE to see some of the directions provided to AI coding agents (e.g., Antigravity CLI, Claude CLI, OpenCode CLI).

About

Creates Lisp packages that reflect C# class (struct, enum, etc.) functionality from a given .Net assembly

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages