Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ members = [
"ruby/extractor",
"unified/extractor",
"unified/extractor/tree-sitter-swift",
"unified/swift-syntax-rs",
"rust/extractor",
"rust/extractor/macros",
"rust/ast-generator",
Expand Down
35 changes: 34 additions & 1 deletion MODULE.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,15 @@ bazel_dep(name = "rules_python", version = "1.9.0")
bazel_dep(name = "rules_shell", version = "0.7.1")
bazel_dep(name = "bazel_skylib", version = "1.9.0")
bazel_dep(name = "abseil-cpp", version = "20260107.1", repo_name = "absl")
bazel_dep(name = "nlohmann_json", version = "3.11.3", repo_name = "json")
bazel_dep(name = "nlohmann_json", version = "3.12.0.bcr.1", repo_name = "json")
bazel_dep(name = "fmt", version = "12.1.0-codeql.1")
bazel_dep(name = "rules_kotlin", version = "2.2.2-codeql.1")
bazel_dep(name = "gazelle", version = "0.50.0")
bazel_dep(name = "rules_dotnet", version = "0.21.5-codeql.1")
bazel_dep(name = "googletest", version = "1.17.0.bcr.2")
bazel_dep(name = "rules_rust", version = "0.69.0")
bazel_dep(name = "rules_swift", version = "4.0.0-rc4")
bazel_dep(name = "swift-syntax", version = "602.0.0.bcr.2")
bazel_dep(name = "zstd", version = "1.5.7.bcr.1")

bazel_dep(name = "buildifier_prebuilt", version = "6.4.0", dev_dependency = True)
Expand Down Expand Up @@ -219,6 +221,37 @@ use_repo(
"swift-resource-dir-macos",
)

# Hermetic Swift toolchain (from swift.org) for building the `swift-syntax`
# based Rust wrapper in `unified/swift-syntax-rs`. This is the official
# `rules_swift` standalone-toolchain extension and is independent of the
# patched prebuilt toolchain wired up via `swift_deps` above.
#
# Bazel cannot auto-select between Linux distributions, so the toolchain is
# registered explicitly for the distribution our CI runs on (ubuntu24.04,
# x86_64). Add further platforms here if CI grows to cover them.
#
# We register the `exec` toolchain (normal host compilation, targeting
# linux/x86_64) rather than the `embedded` one, which targets `os:none`
# (Embedded Swift) and would not match a normal host build.
#
# The Swift version is read from `unified/swift-syntax-rs/.swift-version`, which
# is the single source of truth shared with the local `cargo` build (via
# swiftly / any Swift install) and `swift/Package.swift`.
swift = use_extension("@rules_swift//swift:extensions.bzl", "swift")
swift.toolchain(
name = "swift_toolchain",
swift_version_file = "//unified/swift-syntax-rs:.swift-version",
)
use_repo(
swift,
"swift_toolchain",
"swift_toolchain_ubuntu24.04",
)

register_toolchains(
"@swift_toolchain//:swift_toolchain_exec_ubuntu24.04",
)

node = use_extension("@rules_nodejs//nodejs:extensions.bzl", "node")
node.toolchain(
name = "nodejs",
Expand Down
2 changes: 2 additions & 0 deletions unified/swift-syntax-rs/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/target
/swift/.build
1 change: 1 addition & 0 deletions unified/swift-syntax-rs/.swift-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
6.2.4
55 changes: 55 additions & 0 deletions unified/swift-syntax-rs/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
load("@rules_rust//rust:defs.bzl", "rust_binary", "rust_library", "rust_test")
load("@rules_swift//swift:swift.bzl", "swift_library")

package(default_visibility = ["//visibility:public"])

# The pinned Swift version, shared with the local `cargo` build and
# `swift/Package.swift`. Referenced by the `swift.toolchain` extension in
# //:MODULE.bazel via `swift_version_file`.
exports_files([".swift-version"])

# The Swift FFI shim: wraps swift-syntax and exposes a small C ABI
# (`ssr_parse_json` / `ssr_string_free`). Built with the hermetic Swift
# toolchain registered in //:MODULE.bazel; provides a `CcInfo` that the Rust
# targets below link against.
swift_library(
name = "swift_syntax_ffi",
srcs = ["swift/Sources/SwiftSyntaxFFI/SwiftSyntaxFFI.swift"],
module_name = "SwiftSyntaxFFI",
deps = [
"@swift-syntax//:SwiftParser",
"@swift-syntax//:SwiftSyntax",
],
)

# Safe Rust bindings on top of the C ABI. `build.rs` is intentionally *not*
# wired up here (no `cargo_build_script`): under Bazel the Swift side is
# provided by `:swift_syntax_ffi`, whereas `build.rs` only exists to build the
# Swift shim in the local `cargo` workflow.
rust_library(
name = "swift_syntax_rs",
srcs = ["src/lib.rs"],
edition = "2024",
deps = [":swift_syntax_ffi"],
)

rust_binary(
name = "swift-syntax-parse",
srcs = ["src/main.rs"],
# The Swift toolchain propagates a runfiles-relative RPATH to its runtime
# `.so`s via `swift_syntax_ffi`'s `CcInfo`, but (unlike `swift_binary`)
# `rust_binary` does not copy those libraries into runfiles. Add the
# toolchain files as `data` so the runtime is found at execution time.
# (rules_swift does not yet support statically linking the runtime.)
data = ["@swift_toolchain_ubuntu24.04//:files"],
edition = "2024",
deps = [":swift_syntax_rs"],
)

rust_test(
name = "swift_syntax_rs_test",
size = "small",
crate = ":swift_syntax_rs",
Comment thread
tausbn marked this conversation as resolved.
data = ["@swift_toolchain_ubuntu24.04//:files"],
edition = "2024",
)
14 changes: 14 additions & 0 deletions unified/swift-syntax-rs/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[package]
name = "swift-syntax-rs"
description = "Rust wrapper around the swift-syntax package for parsing Swift source"
version = "0.1.0"
authors = ["GitHub"]
edition = "2024"

[lib]
name = "swift_syntax_rs"
path = "src/lib.rs"

[[bin]]
name = "swift-syntax-parse"
path = "src/main.rs"
180 changes: 180 additions & 0 deletions unified/swift-syntax-rs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
# swift-syntax-rs

A Rust wrapper around the [swift-syntax](https://github.com/swiftlang/swift-syntax)
package, allowing Swift source code to be parsed from Rust.

Parsing is delegated to a small Swift shim (in [`swift/`](swift/)) that links
against `SwiftSyntax`/`SwiftParser` and exposes a tiny C ABI. The Rust crate
builds that shim (via `build.rs`) and provides safe bindings on top of it.

## Output format

The emitted JSON tree preserves the AST's named structure. Every node has a
`kind` and a `range` with `start`/`end` positions (UTF-8 `offset` plus 1-based
`line`/`column`). Beyond that:

- **Tokens** carry `text`, `tokenKind`, and `leadingTrivia`/`trailingTrivia`
arrays of `{ kind, text }` pieces.
- **Layout nodes** (e.g. `functionDecl`) embed their children directly as
members keyed by the child's name in the parent (`name`, `signature`,
`body`, …), alongside `kind`/`range`. Absent optional children are omitted.
- **Collection nodes** (e.g. `codeBlockItemList`) are elided: a list-valued
field is simply a JSON array of its elements (e.g. `parameters`, `statements`).
This drops the collection node's own `kind`/`range`.

Only meaningful trivia is kept — the four comment kinds (`lineComment`,
`blockComment`, `docLineComment`, `docBlockComment`) and `unexpectedText`
(source the parser skipped). Whitespace trivia is dropped, since node ranges
already encode positions.

### Example

Parsing `let x = 1 // c` produces the following (each `range` object is
abbreviated here as `…`):

```jsonc
{
"kind": "sourceFile",
"range": …,
"statements": [ // collection node elided to an array
{
"kind": "codeBlockItem",
"range": …,
"item": {
"kind": "variableDecl",
"range": …,
"attributes": [], // empty collection → empty array
"modifiers": [],
"bindingSpecifier": { // a token
"kind": "token",
"text": "let",
"tokenKind": "keyword(SwiftSyntax.Keyword.let)",
"range": …,
"leadingTrivia": [],
"trailingTrivia": []
},
"bindings": [
{
"kind": "patternBinding",
"range": …,
"pattern": {
"kind": "identifierPattern",
"range": …,
"identifier": { "kind": "token", "text": "x", "tokenKind": "identifier(\"x\")", "range": …, "leadingTrivia": [], "trailingTrivia": [] }
},
"initializer": {
"kind": "initializerClause",
"range": …,
"equal": { "kind": "token", "text": "=", "tokenKind": "equal", "range": …, "leadingTrivia": [], "trailingTrivia": [] },
"value": {
"kind": "integerLiteralExpr",
"range": …,
"literal": {
"kind": "token",
"text": "1",
"tokenKind": "integerLiteral(\"1\")",
"range": …,
"leadingTrivia": [],
"trailingTrivia": [ { "kind": "lineComment", "text": "// c" } ]
}
}
}
}
]
}
}
],
"endOfFileToken": { "kind": "token", "text": "", "tokenKind": "endOfFile", "range": …, "leadingTrivia": [], "trailingTrivia": [] }
}
```

Note how `statements`, `bindings`, `attributes`, and `modifiers` are plain
arrays (their collection nodes are elided), layout children such as
`bindingSpecifier` and `initializer` are embedded by name, and the `// c`
comment rides along as `trailingTrivia` on the token it follows.

## Prerequisites

The build does not depend on any particular version manager. You need:

- **Rust** — pinned to `1.88` by the repo-root [`rust-toolchain.toml`](../../rust-toolchain.toml),
which `rustup` picks up automatically.
- **Swift** — pinned to the version in [`.swift-version`](.swift-version)
(currently `6.2.4`), used to build `swift-syntax` `602.0.0`. Install it any way
you like — [swift.org](https://www.swift.org/install/) or
[swiftly](https://www.swift.org/swiftly/) (which reads `.swift-version`), or a
system package. Just make sure `swift` is on your `PATH` (or point `build.rs`
at it with the `SWIFT` environment variable).

On Debian/Ubuntu the Swift runtime also needs `libncurses6` (and related libs)
available on the system.

## Building & testing

With `cargo` and `swift` on `PATH`:

```sh
cargo build
cargo test
```

If your `swift`/`swiftc` are not on `PATH`, point the build at them explicitly:

```sh
SWIFT=/path/to/swift SWIFTC=/path/to/swiftc cargo build
```
Comment on lines +119 to +123

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it common to have something like this in rust builds that also compile code in other languages? Note that I'm not suggesting to change this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we do it anywhere else, so it's a valid concern. Originally I had swift-syntax-rs much more tightly integrated with the extractor, in which case it made sense to build both at the same time. However, now that we have (for the time being) moved to a model where we call an external binary for the parsing, we could also decouple the builds themselves.


The first build compiles `swift-syntax` and can take several minutes.

## Building with Bazel (CI)

CI builds this crate hermetically with Bazel. A Swift toolchain is downloaded
from swift.org by the official `rules_swift` standalone toolchain extension
(wired up in the repo-root `MODULE.bazel`), `swift-syntax` is pulled from the
Bazel Central Registry, and the FFI shim is compiled as a `swift_library` that
the Rust targets link against. `build.rs` is not used under Bazel; it only
builds the Swift shim for the local `cargo` workflow.

```sh
bazel build //unified/swift-syntax-rs:swift-syntax-parse
bazel test //unified/swift-syntax-rs:swift_syntax_rs_test
bazel run //unified/swift-syntax-rs:swift-syntax-parse < some.swift
```

Requirements:

- **`clang`** must be installed on the runner. `rules_swift` requires the Bazel
CC toolchain to use clang; the repo's `.bazelrc` already sets
`--repo_env=CC=clang`, so no extra flags are needed.
- The registered Swift toolchain currently targets **ubuntu24.04 / x86_64**
only (Bazel cannot auto-select between Linux distributions). Add more
platforms in `MODULE.bazel` (`swift.toolchain` + `register_toolchains`) if CI
grows to cover them.

The Swift compiler version is read from [`.swift-version`](.swift-version) by
both the Bazel toolchain (`swift.toolchain(swift_version_file = …)`) and the
local build, and is kept in sync with the `swift-syntax` release pinned in
`swift/Package.swift`, so local and CI builds behave identically.
Comment thread
tausbn marked this conversation as resolved.
Outdated

## Usage

Library:

```rust
let json = swift_syntax_rs::parse_to_json("let x = 1")?;
println!("{json}");
```

CLI (reads a file argument or stdin, prints the syntax tree as JSON):

```sh
echo 'let x = 1' | cargo run --bin swift-syntax-parse
```

## Layout

- `swift/` — Swift package exposing the `ssr_parse_json` / `ssr_string_free` C ABI.
- `build.rs` — builds the Swift package and emits link/rpath flags (local `cargo` only).
- `BUILD.bazel` — Bazel targets for the hermetic CI build (swift_library + rust targets).
- `src/lib.rs` — safe Rust bindings (`parse_to_json`).
- `src/main.rs` — demo CLI.
Loading