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.
The
parametersDSL acceptsmin:andmax:on aparam,BB.Dsl.Paramstores them, andbb:parameterssays they "bound numeric types" — butBB.Parameter.set/3accepts values outside them. Only the type is validated.Reproduction
Type validation itself works —
set(..., [:gain], "nope")correctly returns aSpark.Options.ValidationError.Cause
BB.Dsl.ParameterTransformer.build_schema_opts/1copies onlytype,docanddefaultout of the%BB.Dsl.Param{}into the generated schema:So the bounds are dropped before they reach validation:
BB.Robot.Runtime.validate_against_schema/3then validates againstSpark.Options, which never sees them.Note on the fix
Passing them through isn't enough —
Spark.Optionshas no:min/:maxschema keys:So it needs either a
{:custom, ...}type wrapping the declared type with a bounds check, or a bounds check afterSpark.Options.validate/2invalidate_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
ArgumentErrorrather than returning an error when handed something wider. A single out-of-rangeset/3from IEx therefore crashes the sensor, the supervisor restarts it, it re-reads the same out-of-range parameter, and it crash-loops. The declaredmax:looks like it should have prevented that.Workaround is to re-validate inside the component's
handle_options/2and keep the previous configuration, which means the bounds end up expressed twice.Either enforcing them or rejecting
min/maxat 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.