From 07067121687bcd3b0534720d203e9a78f2c56248 Mon Sep 17 00:00:00 2001 From: Michael Harp Date: Thu, 23 Jul 2026 07:25:22 -0400 Subject: [PATCH 1/3] Document typecasting The language collection had no page covering conversion between data types. Add lang_typecasting.md, covering calling a data type as a constructor, conversion to numbers, strings, booleans, arrays and hashes, extracting numbers with scanf, and automatic coercion in arithmetic. Every example is verified against OpenVox 8.28.1 with puppet apply. Two behaviors documented here differ from what the upstream new() docstring and the Puppet Core typecasting page describe: * The docstring gives Float.new and Numeric.new an $abs default of true; at runtime the default is false, so Numeric('-42.3') returns -42.3 rather than 42.3. * Automatic string-to-number coercion in arithmetic is an error by default, because the strict setting defaults to error. The page shows explicit conversion instead of relaxing the setting. Conversion is described as something you ask for rather than something Puppet never does, since arithmetic coerces strings to numbers when strict is not error, and interpolation converts any value to a string even under strict=error. Both exceptions are signposted from the top of the page. Part of #407 Co-Authored-By: Claude Opus 4.8 Signed-off-by: Michael Harp --- _data/nav/openvox_8x.yml | 2 + docs/_openvox_8x/lang_data.md | 5 + docs/_openvox_8x/lang_typecasting.md | 334 +++++++++++++++++++++++++++ 3 files changed, 341 insertions(+) create mode 100644 docs/_openvox_8x/lang_typecasting.md diff --git a/_data/nav/openvox_8x.yml b/_data/nav/openvox_8x.yml index dad926c45..ce7021a95 100644 --- a/_data/nav/openvox_8x.yml +++ b/_data/nav/openvox_8x.yml @@ -215,6 +215,8 @@ link: "lang_data_type.html" - text: Abstract data types link: "lang_data_abstract.html" + - text: Typecasting + link: "lang_typecasting.html" - text: Using templates link: "lang_template.html" - text: Embedded Puppet (EPP) template syntax diff --git a/docs/_openvox_8x/lang_data.md b/docs/_openvox_8x/lang_data.md index 20362756f..0b4756223 100644 --- a/docs/_openvox_8x/lang_data.md +++ b/docs/_openvox_8x/lang_data.md @@ -49,3 +49,8 @@ For special abstract data types, which you can use to do more sophisticated or p * [Abstract Data Types](./lang_data_abstract.html) +Converting a value from one data type to another is something you ask for explicitly. To turn a value of one +type into a value of another, such as a string holding a number into an actual number, see: + +* [Typecasting](./lang_typecasting.html) + diff --git a/docs/_openvox_8x/lang_typecasting.md b/docs/_openvox_8x/lang_typecasting.md new file mode 100644 index 000000000..9b0e993e1 --- /dev/null +++ b/docs/_openvox_8x/lang_typecasting.md @@ -0,0 +1,334 @@ +--- +layout: default +title: "Language: Typecasting" +--- + +[data type]: lang_data_type.html +[data types]: lang_data_type.html +[new function]: function.html#new +[scanf function]: function.html#scanf +[strings]: lang_data_string.html +[numbers]: lang_data_number.html +[booleans]: lang_data_boolean.html +[arrays]: lang_data_array.html +[hashes]: lang_data_hash.html +[abstract types]: lang_data_abstract.html +[regexp]: lang_data_regexp.html +[binary]: lang_data_binary.html +[time]: lang_data_time.html + +Every value in the Puppet language has a [data type][], and converting between types is something you ask +for rather than something Puppet does for you. When you have a string that holds a number, or an array of +pairs that ought to be a hash, you convert it by asking for a new value of the type you want. + +Converting never changes the original value. It produces a new value, leaving the one you started with +alone. + +Two things do convert without being asked: arithmetic on a string that looks like a number, and string +interpolation. Both are covered in [automatic coercion](#automatic-coercion-in-arithmetic) below. + +## Calling a data type as a constructor + +To convert a value, call the target data type as though it were a function, passing the value you want to +convert: + +```puppet +notice(Integer('42')) # 42 +notice(Boolean('yes')) # true +notice(String(42)) # 42 +``` + +Calling a data type this way is exactly the same as calling the [`new` function][new function] on it. These +two lines do the same thing: + +```puppet +$a = Integer.new('42') +$b = Integer('42') +``` + +Most people find the shorter form easier to read, so the rest of this page uses it. + +Some conversions take extra arguments. You can pass them by position, or by name using a hash: + +```puppet +notice(Integer('42', 8)) # 34 +notice(Integer({'from' => '42', 'radix' => 8})) # 34 +``` + +Both of those read the string `'42'` as an octal number, giving the decimal value 34. + +If the data type has constraints, the converted value is checked against them and an error is raised if it +doesn't fit. For example, `Integer[0]` only accepts values of zero or more: + +```puppet +Integer[0]('-100') +# Error: Converted value from Integer[0].new() has wrong +# type, expects an Integer[0] value, got Integer[-100, -100] +``` + +You cannot construct most [abstract types][], such as `Variant`. `Optional[T]` and `NotUndef[T]` are +exceptions: they convert exactly as their type argument `T` does, and `Optional[T]` also accepts `undef`: + +```puppet +notice(Optional[Integer]('42')) # 42 +notice(Optional[Integer](undef) =~ Undef) # true +``` + +## Converting to numbers + +For what the resulting values can do once you have them, see [numbers][]. + +### Integer + +`Integer` accepts strings, floats, and booleans. When converting a string, you can give a radix (base) as a +second argument: + +```puppet +notice(Integer('0xFF', 16)) # 255 +notice(Integer('010', 10)) # 10 +``` + +If you don't specify a radix, Puppet detects it from the start of the string: a leading `0b` or `0B` means +binary, `0x` or `0X` means hexadecimal, and a leading `0` means octal. Everything else is decimal. Passing +`default` as the radix asks for this same detection explicitly. + +```puppet +notice(Integer('010')) # 8 +notice(Integer('0b1011')) # 11 +notice(Integer('-42')) # -42 +``` + +Converting a float truncates the fraction rather than rounding it, and a boolean becomes `1` or `0`: + +```puppet +notice(Integer(3.99)) # 3 +notice(Integer(-3.99)) # -3 +notice(Integer(true)) # 1 +notice(Integer(false)) # 0 +``` + +A third argument of `true` makes the result absolute: + +```puppet +notice(Integer(-38, 10, true)) # 38 +``` + +### Float and Numeric + +`Float` always produces a float, adding a `.0` fraction to whole numbers: + +```puppet +notice(Float('3.14')) # 3.14 +notice(Float(3)) # 3.0 +notice(Float(true)) # 1.0 +notice(Float('0xFF')) # 255.0 +``` + +`Numeric` picks the type to suit the value. A value with a decimal point or an exponent becomes a float, +and anything else becomes an integer: + +```puppet +notice(Numeric('3.14')) # 3.14 +notice(Numeric('1e3')) # 1000.0 +notice(Numeric('0xFF')) # 255 +notice(Numeric(true)) # 1 +``` + +Both accept an extra `true` argument to return an absolute value: + +```puppet +notice(Numeric('-42.3')) # -42.3 +notice(Numeric(-42.3, true)) # 42.3 +``` + +> **Note:** conversion to a number is strict about the text it accepts. A string with surrounding whitespace, +> such as `' 42 '`, cannot be converted, and neither can `undef`. Both raise an error rather than +> returning a default. If you need to tolerate stray text, see +> [extracting numbers from strings](#extracting-numbers-from-strings) below. + +## Converting to strings + +`String` converts any value, including arrays and hashes, into a [string][strings]: + +```puppet +notice(String(42)) # 42 +notice(String(true)) # true +notice(String([1,2,3])) # [1, 2, 3] +``` + +A second argument gives a format directive controlling how the value is rendered: + +```puppet +notice(String(255, '%x')) # ff +notice(String(255, '%#x')) # 0xff +notice(String(3.14159, '%.2f')) # 3.14 +``` + +Two defaults are worth knowing. A float with no format is rendered with six decimal places, and `undef` +becomes an empty string: + +```puppet +notice(String(1.0)) # 1.000000 +notice(String(undef) == '') # true +``` + +`String` accepts a large set of format directives, and can take a hash mapping data types to formats so that +values inside an array or hash are each rendered differently. For the full set, see the +[`new` function][new function]. + +## Converting to booleans + +Unlike some languages, Puppet has no notion of "truthy" values to fall back on, so conversion to a +[boolean][booleans] accepts a small, fixed vocabulary. The strings `'true'`, `'yes'`, and `'y'` become `true`, and +`'false'`, `'no'`, and `'n'` become `false`. The comparison ignores case: + +```puppet +notice(Boolean('true')) # true +notice(Boolean('YES')) # true +notice(Boolean('No')) # false +``` + +For numbers, zero is `false` and everything else, including negative numbers, is `true`: + +```puppet +notice(Boolean(0)) # false +notice(Boolean(0.0)) # false +notice(Boolean(1)) # true +notice(Boolean(-1)) # true +``` + +Any other string raises an error, so `Boolean('maybe')` fails rather than guessing. + +## Converting between arrays and hashes + +[Arrays][arrays] and [hashes][hashes] convert into one another, which is a common need when data arrives in +one shape and your code expects the other. + +### To an array + +A hash becomes an array of key/value pairs: + +```puppet +notice(Array({'a' => 1, 'b' => 2})) +# [[a, 1], [b, 2]] +``` + +An array is returned unchanged, an empty hash becomes an empty array, and a [`Binary`][binary] becomes an +array of byte values: + +```puppet +notice(Array(Binary('aGk='))) # [104, 105] +``` + +Anything iterable is turned into an array of the things it iterates over, which is easy to trip over. A +string iterates over its characters, and an integer iterates over the numbers below it: + +```puppet +notice(Array('hello')) # [h, e, l, l, o] +notice(Array(1)) # [0] +``` + +When what you actually want is "wrap this in an array unless it already is one", pass a second argument of +`true`: + +```puppet +notice(Array('hello', true)) # [hello] +notice(Array([1, 2], true)) # [1, 2] +``` + +Note that `undef` wraps to an empty array, not to an array containing `undef`: + +```puppet +notice(Array(undef, true)) # [] +``` + +### To a hash + +An array of pairs becomes a hash, and so does a flat array with an even number of entries: + +```puppet +notice(Hash([['a', 1], ['b', 2]])) # {a => 1, b => 2} +notice(Hash(['a', 1, 'b', 2])) # {a => 1, b => 2} +``` + +An empty array becomes an empty hash, and a hash is returned unchanged. An array with an odd number of +entries raises an `odd number of arguments for Hash` error. + +### Tuple and Struct + +`Tuple` converts exactly as `Array` does, and `Struct` exactly as `Hash` does. The difference is that the +result is then checked against the type you asked for: + +```puppet +notice(Tuple[String, Integer](['a', 1])) # [a, 1] +notice(Struct[{'a' => Integer}]([['a', 1]])) # {a => 1} +``` + +## Extracting numbers from strings + +Conversion requires the whole string to be a number. To pull a number out of a longer piece of text, use the +[`scanf` function][scanf function], which takes a format and always returns an array of what it found: + +```puppet +notice(scanf('42 apples', '%d')) # [42] +notice('2.5kg'.scanf('%f')) # [2.5] +``` + +If nothing matches, you get an empty array rather than an error, which makes `scanf` a good way to handle +input that might not contain a number at all: + +```puppet +notice(scanf('no digits', '%d')) # [] +``` + +## Automatic coercion in arithmetic + +Arithmetic operators can coerce a string to a number, but by default OpenVox treats doing so as an error: + +```puppet +notice('1' + 1) +# Error: The string '1' was automatically coerced to the +# numerical value 1 +``` + +This is controlled by the `strict` setting, which defaults to `error`. Setting it to `warning` lets the +arithmetic proceed and logs a warning instead; setting it to `off` silences the message entirely. Rather +than relaxing the setting, convert explicitly, which works under any value of `strict`: + +```puppet +notice(Integer('1') + 1) # 2 +``` + +String interpolation is a separate matter and is always allowed. Putting a value in a string converts it +without complaint: + +```puppet +$n = 42 +notice("interpolated:${n}") # interpolated:42 +``` + +## Other conversions + +A string becomes a [`Regexp`][regexp], which is useful when the pattern is built at runtime or comes from +data: + +```puppet +$r = Regexp('[a-z]+\.com') +notice('foo.com' =~ $r) # true +``` + +A string naming a data type becomes that data type: + +```puppet +notice(Type('Integer') == Integer) # true +``` + +The [`Timestamp` and `Timespan`][time] types have their own rich set of conversions from strings, numbers, +and hashes, covered on their own page. + +## Further reading + +* The [`new` function][new function] documents every conversion signature in full, including the complete + list of string format directives. +* [Data type syntax][data types] covers how data types are written and where you can use them. +* [Abstract data types][abstract types] covers `Optional`, `NotUndef`, `Variant`, and the rest. From b410edff8c6b6c0032b224f086e8df432b87148f Mon Sep 17 00:00:00 2001 From: Michael Harp Date: Thu, 23 Jul 2026 07:33:15 -0400 Subject: [PATCH 2/3] Widen the automatic coercion heading to cover interpolation The section documents both arithmetic coercion and string interpolation, but the heading named only arithmetic, so the pointer from the top of the page under-described where it led. Drop the qualifier and update the in-page link. Co-Authored-By: Claude Opus 4.8 Signed-off-by: Michael Harp --- docs/_openvox_8x/lang_typecasting.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/_openvox_8x/lang_typecasting.md b/docs/_openvox_8x/lang_typecasting.md index 9b0e993e1..6ee361349 100644 --- a/docs/_openvox_8x/lang_typecasting.md +++ b/docs/_openvox_8x/lang_typecasting.md @@ -25,7 +25,7 @@ Converting never changes the original value. It produces a new value, leaving th alone. Two things do convert without being asked: arithmetic on a string that looks like a number, and string -interpolation. Both are covered in [automatic coercion](#automatic-coercion-in-arithmetic) below. +interpolation. Both are covered in [automatic coercion](#automatic-coercion) below. ## Calling a data type as a constructor @@ -281,7 +281,7 @@ input that might not contain a number at all: notice(scanf('no digits', '%d')) # [] ``` -## Automatic coercion in arithmetic +## Automatic coercion Arithmetic operators can coerce a string to a number, but by default OpenVox treats doing so as an error: From 244b844be4b44473d7277b07735fe66b5539a54e Mon Sep 17 00:00:00 2001 From: Michael Harp Date: Mon, 27 Jul 2026 07:30:13 -0400 Subject: [PATCH 3/3] Explain why the absolute-value example passes a radix The Integer section said the radix applies when converting a string, but the absolute-value example passed an Integer with a radix of 10, which reads as inconsistent. Integer has a single positional dispatch, (from, radix, abs), where the radix is typed Variant[Default, Integer[2,2], Integer[8,8], Integer[10,10], Integer[16,16]]. A Boolean cannot occupy that slot, so Integer(-38, true) is an argument-mismatch error and the radix must be supplied to reach the third argument even when it does not apply. The upstream new() docstring advertises a two-argument Integer.new(Variant[Numeric, Boolean], Boolean) overload that the dispatch does not implement. Keep a working example, add a string case where the radix genuinely applies, and use default as the placeholder for non-string input. Say positionally why the argument is there. Verified against OpenVox 8.28.1 with puppet apply. Co-Authored-By: Claude Opus 5 Signed-off-by: Michael Harp --- docs/_openvox_8x/lang_typecasting.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/_openvox_8x/lang_typecasting.md b/docs/_openvox_8x/lang_typecasting.md index 6ee361349..bd74ff1c7 100644 --- a/docs/_openvox_8x/lang_typecasting.md +++ b/docs/_openvox_8x/lang_typecasting.md @@ -107,10 +107,13 @@ notice(Integer(true)) # 1 notice(Integer(false)) # 0 ``` -A third argument of `true` makes the result absolute: +A third argument of `true` makes the result absolute. The radix comes first positionally, so you must +still supply it to reach that third argument, even for an input where it doesn't apply. Pass `default` +to keep the automatic detection: ```puppet -notice(Integer(-38, 10, true)) # 38 +notice(Integer('-0x26', 16, true)) # 38 +notice(Integer(-38, default, true)) # 38 ``` ### Float and Numeric