Skip to content

Emit range-based arity check throwing ArgumentsRangeMismatch#19

Merged
tsapeta merged 1 commit into
mainfrom
tsapeta/emit-arguments-range-mismatch-exception
Jun 15, 2026
Merged

Emit range-based arity check throwing ArgumentsRangeMismatch#19
tsapeta merged 1 commit into
mainfrom
tsapeta/emit-arguments-range-mismatch-exception

Conversation

@tsapeta

@tsapeta tsapeta commented Jun 14, 2026

Copy link
Copy Markdown
Collaborator

Why

The synthesized @JS function binding validated arity with an exact arguments.count == <total> guard and threw an ad-hoc Exception(name: "InvalidArgumentCount", ...). That over-rejects an omitted trailing optional, ignores author-written default values, and isn't a typed exception. A function with trailing defaulted or optional parameters accepts a range of arities, not one exact count.

Paired with expo/expo#46901, which adds the public Exceptions.ArgumentsRangeMismatch exception this generated code throws. The two must land together: the code emitted here references that core exception.

How

In DecorateModuleBuilder, compute required (total minus the trailing run of parameters that have a default value or an optional type) and max (total). Emit an exact guard when required == max, otherwise guard arguments.count >= required && arguments.count <= max, both throwing Exceptions.ArgumentsRangeMismatch.

When the trailing run is non-empty, decode the required prefix once and switch on arguments.count:

  • an omitted defaulted parameter drops its label from the call so Swift applies the default;
  • an omitted optional parameter is passed nil;
  • a present slot decodes by its static type in the branch that has it (arguments[i] traps past count, so a slot the caller didn't pass is never indexed).

Also shared isOptionalType via MacroHelpers (removing the private copy in RecordMacro).

Example

A function with a trailing defaulted parameter:

@ExpoModule
final class MyModule: Module {
  @JS
  func resize(width: Int, height: Int = 100) -> Bool { true }
}

now generates a range guard and a per-arity call:

public func _decorateModule(object: borrowing JavaScriptObject, in runtime: JavaScriptRuntime, appContext: AppContext) throws {
  object.setProperty("resize") { [self] this, arguments in
    guard arguments.count >= 1 && arguments.count <= 2 else {
      throw Exceptions.ArgumentsRangeMismatch((functionName: "resize", received: arguments.count, required: 1, maximum: 2))
    }
    let arg0 = try arguments.unownedValue(at: 0).asInt()
    let result = switch arguments.count {
    case 1:
      self.resize(width: arg0)
    default:
      let arg1 = try arguments.unownedValue(at: 1).asInt()
      self.resize(width: arg0, height: arg1)
    }
    return result.toJavaScriptValue(in: runtime)
  }
}

When every parameter is required the guard collapses to an exact arguments.count == <total> check and the body is a flat decode-then-call (no switch).

Test Plan

swift test, 86 passing, including new expansion cases:

  • trailing defaulted parameter (range guard + per-arity switch);
  • trailing optional parameter (nil when omitted, Void return path);
  • all-omittable signature (required == 0).

Existing exact-arity expectations were updated to the new exception form.

@tsapeta tsapeta force-pushed the tsapeta/emit-arguments-range-mismatch-exception branch 2 times, most recently from 730a91f to 58bddda Compare June 14, 2026 20:54
@tsapeta tsapeta changed the title [apple][macros] Emit range-based arity check throwing ArgumentsRangeMismatch Emit range-based arity check throwing ArgumentsRangeMismatch Jun 14, 2026
@tsapeta tsapeta force-pushed the tsapeta/emit-arguments-range-mismatch-exception branch from 58bddda to 217c5a9 Compare June 14, 2026 20:59
…ismatch

The synthesized `@JS` function binding validated arity with an exact
`arguments.count == <total>` guard and threw an ad-hoc
`Exception(name: "InvalidArgumentCount", ...)`, over-rejecting omitted
trailing optionals and ignoring author-written default values.

Compute `required` (total minus the trailing run of params that have a
default value or an optional type) and `max` (total). Emit an exact guard
when `required == max`, otherwise `guard arguments.count >= required &&
arguments.count <= max`, both throwing core's new
`Exceptions.ArgumentsRangeMismatch`. When the trailing run is non-empty,
decode the required prefix once and `switch` on `arguments.count`: an
omitted defaulted param drops its label (Swift applies the default), an
omitted optional param is passed `nil`.

Share `isOptionalType`, `isOmittable`, and `hasDefaultValue` via
`MacroHelpers` (removing the private copy of `isOptionalType` in
`RecordMacro`).
@tsapeta tsapeta force-pushed the tsapeta/emit-arguments-range-mismatch-exception branch from 217c5a9 to c820879 Compare June 14, 2026 21:00
@tsapeta tsapeta marked this pull request as ready for review June 14, 2026 21:14
@tsapeta tsapeta requested a review from Kudo June 14, 2026 21:14
@tsapeta tsapeta merged commit b0bd993 into main Jun 15, 2026
1 check passed
@tsapeta tsapeta deleted the tsapeta/emit-arguments-range-mismatch-exception branch June 15, 2026 20:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants