Migrating to Rad v0.9¶
Version 0.9 unifies rad block keywords, introduces changes to error handling operators, renames a built-in function, shortens parse_epoch unit names, and enables invocation logging by default. This guide covers what changed and how to update your scripts.
Breaking Change: request and display Keywords Removed¶
What Changed¶
The three separate rad block keywords (rad, request, display) have been unified into a single rad keyword. The rad keyword now dispatches on the source type at runtime:
- URL string - fetches JSON from the URL (replaces
request) - List or map - extracts data in-memory (replaces
display) - No source - operates on existing variables (replaces sourceless
display)
Old Syntax (No Longer Works)¶
request "https://api.example.com/users":
fields Name, Age
display data:
fields Name, Age
New Syntax¶
rad "https://api.example.com/users":
noprint
fields Name, Age
rad data:
fields Name, Age
Important: request Blocks Never Printed¶
request blocks only fetched data and populated fields - they never printed a table. The unified rad block prints by default, so when migrating from request you'll typically want to add noprint to preserve the old behavior.
display blocks already printed, so replacing display with rad requires no other changes.
Migration Steps¶
- Replace
requestwithradand addnoprintif you don't want table output - Replace
displaywithrad- no other changes needed - Run
rad checkon your scripts to catch any remaining uses
Breaking Change: get_stash_dir Renamed¶
What Changed¶
get_stash_dir was renamed to get_stash_path to better reflect its behavior - it returns a path (often to a file), not necessarily a directory.
Old Syntax (No Longer Works)¶
path = get_stash_dir()
path = get_stash_dir("data/config.json")
New Syntax¶
path = get_stash_path()
path = get_stash_path("data/config.json")
Migration Steps¶
- Find all uses of
get_stash_dirin your scripts - Replace with
get_stash_path- the signature and behavior are identical
Breaking Change: ?? Now Fires on Null¶
What Changed¶
?? is now a true null-coalescing operator. It fires when the left side is null or an error, where previously it only fired on errors.
Before (v0.8)¶
m = {"key": null}
print(m["key"] ?? "fallback") // printed: null
print(null ?? "default") // printed: null
After (v0.9)¶
m = {"key": null}
print(m["key"] ?? "fallback") // prints: fallback
print(null ?? "default") // prints: default
Why¶
This aligns ?? with developer expectations from JS/Kotlin/Swift where ?? is a null-coalescing operator. The old error-only behavior is now available via the new catch operator (see below).
Migration¶
If you relied on null passing through ??, use catch instead:
// If you want error-only catching (old behavior), use catch:
result = maybe_error_value catch "default"
New: catch Operator¶
The catch operator provides the old ?? error-only behavior as an inline expression. It catches errors but lets null values pass through.
count = parse_int(input_str) catch 0
data = parse_json(raw) catch {}
This is distinct from the catch: block syntax. The operator form is an inline expression; catch: is a block attached to statements.
Breaking Change: Strict + Concatenation¶
What Changed¶
The + operator no longer implicitly converts int, float, or bool to strings. Both operands must be the same type (with the exception that errors behave like strings for concatenation).
Before (v0.8)¶
print("count: " + 5) // printed: count: 5
print("pi: " + 3.14) // printed: pi: 3.14
print("flag: " + true) // printed: flag: true
err = error("oops")
print(err + 123) // printed: oops123
After (v0.9)¶
print("count: " + 5) // error: RAD30002
print("pi: " + 3.14) // error: RAD30002
print("flag: " + true) // error: RAD30002
err = error("oops")
print(err + 123) // error: RAD30002
Why¶
Implicit coercion was asymmetric ("hi" + 5 worked, but 5 + "hi" errored) and used a different conversion path from string interpolation. Making + strict catches type bugs at the point of error rather than silently producing unexpected strings.
Migration¶
Use interpolation (preferred) or str() for explicit conversion:
// Interpolation - handles any type, recommended
print("count: {5}")
print("pi: {3.14}")
// Explicit conversion with str()
print("count: " + str(5))
print("pi: " + str(3.14))
Note: errors still behave like strings for concatenation ("s" + error("e"), error("e") + "s", and error("a") + error("b") all work).
Breaking Change: parse_epoch Unit Names Shortened¶
What Changed¶
The unit parameter for parse_epoch now uses short names to match the rest of the API (now() epoch keys, convert_duration/parse_duration enums).
Old Syntax (No Longer Works)¶
time = parse_epoch(1712345678000, unit="milliseconds")
time = parse_epoch(1712345678000000, unit="microseconds")
time = parse_epoch(1712345678000000000, unit="nanoseconds")
New Syntax¶
time = parse_epoch(1712345678000, unit="millis")
time = parse_epoch(1712345678000000, unit="micros")
time = parse_epoch(1712345678000000000, unit="nanos")
Migration¶
Replace "milliseconds" with "millis", "microseconds" with "micros", and "nanoseconds" with "nanos" in any parse_epoch calls.
Error Messages¶
If you run a script that still uses request or display, you'll see:
error[RAD40008]: 'request' blocks have been removed. Use 'rad' instead.
--> script.rad:1:1
|
1 | request "https://api.example.com/users":
| ^^
|
= help: See migration guide: https://amterp.github.io/rad/migrations/v0.9/
= info: rad explain RAD40008
If you run a script that still uses get_stash_dir, you'll see a helpful error:
error: Cannot invoke unknown function: get_stash_dir
hint: get_stash_dir was renamed to get_stash_path.
See: https://amterp.github.io/rad/migrations/v0.9/
If you use the old long-form unit names with parse_epoch, you'll see:
error: parse_epoch unit "milliseconds" is no longer valid
help: Unit names were shortened in v0.9. Use "millis" instead.
See: https://amterp.github.io/rad/migrations/v0.9/
Behavior Change: Invocation Logging Enabled by Default¶
What Changed¶
Invocation logging is now enabled by default. Each time you run a Rad script, Rad logs basic metadata (script path, timestamp, version, duration) to ~/.rad/logs/invocations.jsonl. Previously, this was opt-in.
Arguments are not logged by default - only the metadata listed above.
Why¶
Invocation logs are most useful when they're already there - for example, rad check --from-logs can bulk-check your recently-used scripts after an upgrade. Enabling logging by default means these tools work out of the box.
How to Opt Out¶
If you prefer to disable logging, set enabled = false in ~/.rad/config.toml:
[invocation_logging]
enabled = false
For more details on all available settings, see the Configuration guide.
Breaking Change: trim_prefix / trim_suffix Behavior Changed¶
What Changed¶
trim_prefix and trim_suffix now remove a literal prefix/suffix string, rather than stripping a character set. The old character-set stripping behavior is available via trim_left and trim_right.
Before (v0.8) - Character-Set Stripping¶
In v0.8, trim_prefix removed all leading characters that appeared in the given string, similar to Go's strings.TrimLeft. It treated the argument as a set of characters, not a literal string:
trim_prefix("aaabbb", "a") // -> "bbb" (stripped all leading 'a' chars)
trim_suffix("bbbccc", "c") // -> "bbb" (stripped all trailing 'c' chars)
trim_prefix("abcfoo", "abc") // -> "foo" (stripped all leading 'a', 'b', or 'c' chars)
trim_prefix("cabfoo", "abc") // -> "foo" (same result - order didn't matter, it was a char set)
After (v0.9) - Literal Prefix/Suffix Removal¶
Now, trim_prefix removes the argument as a literal prefix. If the string doesn't start with that exact prefix, it's returned unchanged:
trim_prefix("aaabbb", "a") // -> "aabbb" (removed one literal "a" prefix)
trim_suffix("bbbccc", "c") // -> "bbbcc" (removed one literal "c" suffix)
trim_prefix("abcfoo", "abc") // -> "foo" (removed literal "abc" prefix)
trim_prefix("cabfoo", "abc") // -> "cabfoo" (no match - "cabfoo" doesn't start with "abc")
The old character-set behavior is now available as trim_left and trim_right:
trim_left("aaabbb", "a") // -> "bbb" (old trim_prefix behavior)
trim_right("bbbccc", "c") // -> "bbb" (old trim_suffix behavior)
Migration¶
If you were using trim_prefix or trim_suffix to strip characters from a set, switch to trim_left / trim_right respectively.