Skip to content

Parameter min/max are parsed and documented but never enforced #230

Description

@jimsynz

The parameters DSL accepts min: and max: on a param, BB.Dsl.Param stores them, and bb:parameters says they "bound numeric types" — but BB.Parameter.set/3 accepts values outside them. Only the type is validated.

Reproduction

defmodule BoundsRepro.Robot do
  use BB

  parameters do
    param :gain, type: :float, default: 0.5, min: 0.0, max: 1.0
  end

  topology do
    link :base_link do
    end
  end
end

{:ok, _pid} = BoundsRepro.Robot.start_link([])

BB.Parameter.get!(BoundsRepro.Robot, [:gain])
#=> 0.5

BB.Parameter.set(BoundsRepro.Robot, [:gain], 1000.0)
#=> :ok          # expected {:error, _}

BB.Parameter.get!(BoundsRepro.Robot, [:gain])
#=> 1000.0

Type validation itself works — set(..., [:gain], "nope") correctly returns a Spark.Options.ValidationError.

Cause

BB.Dsl.ParameterTransformer.build_schema_opts/1 copies only type, doc and default out of the %BB.Dsl.Param{} into the generated schema:

defp build_schema_opts(%Param{} = param) do
  opts = [type: convert_param_type(param.type)]
  opts = if param.doc, do: Keyword.put(opts, :doc, param.doc), else: opts
  opts = if param.default != nil, do: Keyword.put(opts, :default, param.default), else: opts
  opts
end

So the bounds are dropped before they reach validation:

BoundsRepro.Robot.__bb_parameter_schema__()
#=> [{[:gain], [default: 0.5, type: :float]}]

BB.Robot.Runtime.validate_against_schema/3 then validates against Spark.Options, which never sees them.

Note on the fix

Passing them through isn't enough — Spark.Options has no :min/:max schema keys:

** (ArgumentError) invalid Spark.Options schema. Reason: unknown options [:min],
   valid options are: [:type, :required, :default, :keys, :deprecated, :private?,
   :hide, :as, :snippet, :links, :doc, :subsection, :type_doc, :type_spec]

So it needs either a {:custom, ...} type wrapping the declared type with a bounds check, or a bounds check after Spark.Options.validate/2 in validate_against_schema/3. The latter keeps the error shape separate from Spark's, which may or may not be desirable.

Why it bites

Parameters are the documented way to expose runtime-tunable hardware settings, and hardware registers have hard widths. In my case a tap-detection timing is a 7-bit register field, and the driver raises ArgumentError rather than returning an error when handed something wider. A single out-of-range set/3 from IEx therefore crashes the sensor, the supervisor restarts it, it re-reads the same out-of-range parameter, and it crash-loops. The declared max: looks like it should have prevented that.

Workaround is to re-validate inside the component's handle_options/2 and keep the previous configuration, which means the bounds end up expressed twice.

Either enforcing them or rejecting min/max at compile time would both be fine — the current state is the worst of the three, since the declaration reads as a constraint but isn't one.

Found against bb 0.29.0.

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