Skip to content

Add try(value, default) special form#17

Merged
myzie merged 1 commit into
mainfrom
try-special-form
May 8, 2026
Merged

Add try(value, default) special form#17
myzie merged 1 commit into
mainfrom
try-special-form

Conversation

@myzie
Copy link
Copy Markdown
Contributor

@myzie myzie commented May 8, 2026

Summary

Adds a try(value, default) special form so callers can opt in to graceful failure on a per-expression basis without weakening the strict-missing-key default.

try(int(s), 0)                                      // parse with fallback
try(find(events, it.kind == "purchase").user, "—")  // optional chain
try(user.nickname, nil) || "(none)"                 // present nil

What it traps

  • Anything wrapping ErrEvaluate: missing fields/keys, nil selectors, out-of-range indices, type-coercion failures from int / float / etc.

What it does not trap

  • Raw context.Canceled and context.DeadlineExceeded — cancellation must remain observable.
  • Anything wrapping ErrCompile — defensive; expr should not surface these at Run time, but if it does, that signals a programmer mistake try is not the right place to swallow.

The default expression is lazy: it only evaluates when the primary failed. That means callers can supply expensive or side-effecting fallbacks without paying for them on the success path.

Implementation

  • Registered in higherOrderForms and higherOrderNames so dispatch and the did-you-mean suggester pick it up automatically.
  • Identifier shadowing (env or WithFunctions) wins over the form, matching every other special form.

Tests and docs

  • higher_order_test.go: 12 new tests covering success path, default-on-eval-error, default-on-type-error, lazy default (panicking if invoked), default error propagation, arity, ctx cancellation passthrough, env / func shadowing, nested composition, and try(...) || fallback interop.
  • suggest_test.go: tightened to avoid relying on the exact higher-order form count.
  • docs/reference/spec.md and llms.txt: new entry plus a paragraph spelling out the trap/no-trap semantics.

Test plan

  • go test ./...
  • go test -race ./...
  • FuzzCompile 20s
  • FuzzEval 20s

What:
- New higher-order special form `try(value, default)` registered in
  the same dispatch table as map/filter/etc. Evaluates `value`; if
  evaluation returns an ErrEvaluate, evaluates and returns `default`
  instead. The default is lazy and only runs when the primary fails.
- Trapped: anything wrapping ErrEvaluate (missing keys, nil selectors,
  out-of-range indices, type-coercion failures from int/float/etc.).
- Not trapped: raw context.Canceled / context.DeadlineExceeded so
  cancellation stays observable, and anything wrapping ErrCompile.
- Spec, llms.txt, and suggester (via higherOrderNames) updated.
- Tightened TestSuggest_UndefinedIdentAvailableList so it does not
  rely on the exact higher-order form count.

Why:
- Strict missing-key access stays the default; typos must fail loudly.
  try is the explicit opt-in for "I know this might miss; here is a
  fallback." Combined with operand-returning || it covers the common
  optional-field case without adding a `default(x, y)` builtin or `??`
  operator. See docs/design/language-evolution.md (step 2).
@myzie myzie merged commit e1566f4 into main May 8, 2026
1 check passed
@myzie myzie deleted the try-special-form branch May 8, 2026 14:22
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.

1 participant