Skip to content

JExtract: report every skipped declaration; add failOnSkippedDeclarations - #3

Open
piercifani wants to merge 2 commits into
jni/02-closures-jextracted-typesfrom
jni/03-skipped-declarations
Open

piercifani wants to merge 2 commits into
jni/02-closures-jextracted-typesfrom
jni/03-skipped-declarations

Conversation

@piercifani

@piercifani piercifani commented Nov 3, 2026 •

Copy link
Copy Markdown

Stack 3/21 · base: jni/02-closures-jextracted-types · previous: #2 · next: #5

Problem

jextract drops declarations it can't translate, and in JNI mode most drops were invisible:

  • Skips during analysis are logged as warnings ("Failed to import").
  • Skips during JNI translation were logged at debug level only. A function returning [T?] or [[K: V]], for example, just doesn't appear in the Java output, with nothing in the build log.
  • Setting "logLevel": "debug" in swift-java.config didn't help: the CLI's default --log-level (info) always overwrote the config's value.
  • Failed translations weren't cached. The cache is [ExtractedFunc: TranslatedFunctionDecl], and storing nil removes the key, so each failure was re-translated on every printing pass.

For a project bridging thousands of declarations, nobody can tell what was lost without diffing the generated Java against the Swift API.

Changes

  • JNI translation failures are logged as warnings, in the same form as analysis skips: Failed to translate: 'Collections.sparse()' in module 'M'; <reason>.
  • Failed translations are cached (updateValue(nil, forKey:), with an optional value type), so each is reported once.
  • New config option failOnSkippedDeclarations (default false). After generating, jextract fails if anything was skipped during analysis or JNI translation, and the error lists every declaration with its reason. The list is in the error itself because the tool exits through fatalError, and log lines written to a piped stdout (as under the SwiftPM plugin) are lost at that point.
  • --log-level only overrides the config's logLevel when it's passed explicitly.
  • makeSwiftJavaAnalyzer takes an optional diagnostics sink; analysis skips are collected through the existing SwiftExtractDiagnosticsSink.
  • Regenerated SwiftJavaConfigFile.md (generate-config-docs --check passes).

Example with failOnSkippedDeclarations: true:

Fatal error: Failed with error: jextract skipped 2 declaration(s) in module 'G08Collections' and 'failOnSkippedDeclarations' is set:
  - Collections.sparse(): unsupportedSwiftType(Tag?, ...)
  - Collections.nested(): unsupportedSwiftType([String: Int64], ...)

Why Skip needs it

Skip's own bridge fails the build on anything it can't bridge, so moving a module onto jextract has to keep that guarantee. In a test suite of small Skip-shaped modules, some drops produced no log line at all, even with logLevel set to debug.

Noise check

The JNI sample now prints 4 distinct new warnings, each a real drop: Data.init(bytes:count:) and Data.withUnsafeBytes(_:) (unsafe pointers), GenericEnumWithValue.some and MyID.rawValue (generic T).

Kotlin in this PR

The Kotlin sample turns on failOnSkippedDeclarations in both modules' swift-java.config. From this PR up, its build fails if jextract drops any public declaration, so every Kotlin test in the stack runs against complete bindings. Checked by adding an unsupported -> [Item?] function, which fails the build with it listed.

Testing

  • Kotlin sample: ./gradlew :Samples:SwiftJavaExtractJNIKotlinSampleApp:test passes at this point in the stack.
  • New JNISkippedDeclarationsTests: a translation failure is recorded once after both the Java and Swift passes, analysis skips reach the sink, and failIfSkipped throws only when configured, with the list in the message.
  • Full swift test --disable-experimental-prebuilts --disable-sandbox passes.
  • ./gradlew :Samples:SwiftJavaExtractJNISampleApp:test passes 275/275.
  • End to end through the SwiftPM plugin: a module with two unsupported functions builds with two warnings by default and fails with both listed when the option is on. With "logLevel": "debug" in the config, debug output now appears; before, only the unconditional [debug][swift-java] prints did.
  • swift format lint --strict on changed files; git diff --check.

Not in this PR

  • FFM mode has the same uncached-failure pattern in FFMSwift2JavaGenerator+JavaTranslation.swift; left as is.
  • Debug-level logs for protocol requirements skipped from existential box dispatch (an internal path) are unchanged.

🤖 Generated with Claude Code

piercifani and others added 2 commits November 3, 2026 17:35
…rations

Declarations that analysis accepted but JNI translation could not handle were
dropped with a debug-level log line only, so missing bindings went unnoticed
(for example `[T?]` and `[[K: V]]` return types). Even `"logLevel": "debug"`
in swift-java.config didn't show them, because the CLI's default `--log-level`
always overwrote the config's value.

- Log JNI translation failures as warnings, in the same form as analysis skips.
- Cache failed translations: assigning nil to the cache dictionary removed the
  key, so every failure was re-translated (and re-reported) on each pass.
- Add `failOnSkippedDeclarations`: after generating, jextract fails and lists
  every declaration skipped during analysis or JNI translation. The list is part
  of the error, since the tool exits through fatalError and log lines on a
  piped stdout aren't flushed.
- Only override the config's `logLevel` when `--log-level` is passed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With failOnSkippedDeclarations, the sample's bindings must cover every public
declaration: jextract fails instead of silently dropping one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant