Skip to content

RFC: or return syntax#53

Open
ctrlaltmilk wants to merge 1 commit intoluau-lang:masterfrom
ctrlaltmilk:or-return-syntax
Open

RFC: or return syntax#53
ctrlaltmilk wants to merge 1 commit intoluau-lang:masterfrom
ctrlaltmilk:or-return-syntax

Conversation

@ctrlaltmilk
Copy link

A small syntax extension to make handling nil values easier and more concise.

Rendered

@dphfox
Copy link

dphfox commented Aug 23, 2024

I would really desperately want to see this - my code is littered with places this would be incredible - but a similar idea I mentioned a while back about "return in expressions" got shot down.

I really hope there is an expression of this design that can be agreed upon.


## Summary

The `or return` syntax is a more compact, understandable way to end execution if an expression evaluates to `nil`.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inconsistent with usual truthiness test. What if the expression evaluates to false?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably should have clarified here - the main use case is for expressions that can return nil, but the already-established truthiness rules would be respected.

@bradsharp
Copy link
Contributor

Would or break also make sense? I wonder if we could just allow return and break in expressions where they would otherwise make sense.

Comment on lines +41 to +43
local humanoid = partParent:FindFirstChild("Humanoid") or return

humanoid.Health = 0
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is just if local which is proposed in a different RFC, so this isn't a strong motivator.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if local does also solve this problem, but it also creates a new indentation level which is counter to the goals of this RFC.

```
and compact it into a single, simpler statement:
```lua
local var = foo() or return false
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't compose with multrets.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not? As far as I can tell, the only possible reason it wouldn't is if commas were used somewhere else in the expression, which is solved by using a pair of parentheses.

@alexmccord
Copy link
Contributor

or break could work but or continue won't work, which is an unfortunate problem that argues against this design.


`or return` can be implemented as a binary operation with an optional right side, returning `nil` if no value is given.

The parsing step would be fairly simple as well. Since the new syntax is a way to express an already-supported feature more concisely, it can be transformed back into its original form in the parser. This means no changes are necessitated on the compiler side. If specified in the middle of an expression, it can temporarily store the value on its left side to perform the nil check, then compute the rest of the expression using the original copy. For example,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Parser can't rewrite the AST beyond its local context, so AST changes are required for this feature to work.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's understandable. Is there some form of AST expansion/lowering pass that this could fit into, or would this need to be supported in the compiler as well?

@ctrlaltmilk
Copy link
Author

Would or break also make sense? I wonder if we could just allow return and break in expressions where they would otherwise make sense.

I did consider allowing return and break as expressions, but decided that would lead to confusing edge cases when called from a context where those statements wouldn't apply.

@dphfox
Copy link

dphfox commented Aug 28, 2024

Here's an idea, or else.

local humanoid = partParent:FindFirstChild("Humanoid") or else return

It's never valid Luau to have the or keyword before an else keyword, so that sequence should be easy enough to detect. That would then allow proper support for or else continue.

And hey, it reads like posh English, that's fun too :)

@gaymeowing
Copy link
Contributor

gaymeowing commented Oct 21, 2024

Here's an idea, or else.

local humanoid = partParent:FindFirstChild("Humanoid") or else return

It's never valid Luau to have the or keyword before an else keyword, so that sequence should be easy enough to detect. That would then allow proper support for or else continue.

And hey, it reads like posh English, that's fun too :)

Feel like it should be orelse to be consistent with elseif, sure its another operator (I think operator is the correct term?) but I don't think anyones naming variables orelse

@gaymeowing
Copy link
Contributor

Also this would have to be

or return ... end

because otherwise this would return the result of PivotTo, as we can't have implicit whitespace

or return
model:PivotTo(CFrame.Identity)

Copy link

@ccuser44 ccuser44 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't make to have in Luau and is syntactical bloat. And frankly, just looks kinda ugly.

@Wunder-Wulfe
Copy link

Wunder-Wulfe commented Feb 23, 2025

I personally do not like the structure of this syntax. I would prefer something along the lines C# with their Null-conditional Operators for some of the use cases listed.

Roblox Type Explanations

This section is for Luau users unfamiliar with Roblox functions and conventions:

hit, object, and part are conventional names for objects under the parent type Instance.

index<Instance, "Parent"> -> Instance?, as the Parent property is nullable.

parentInstance:FindFirstChildOfClass(className) returns Instance? as the object may not be found as a child of parentInstance.

Humanoid is a sub-class of Instance.

index<Instance, "Name"> -> string.

local human: Humanoid? = hit.Parent?:FindFirstChildOfClass("Humanoid");
--[[
eq:

local human: Humanoid? = nil;
do 
    local temp = hit.Parent;
    if temp ~= nil then human = temp:FindFirstChildOfClass("Humanoid"); end
end
]]

-- ...

local name: string? = object:FindFirstChildOfClass("ObjectValue")?.Name;
--[[
eq:

local name: string? = nil;
do 
    local temp = object:FindFirstChildOfClass("ObjectValue");
    if temp ~= nil then name = temp.Name; end
end
]]


-- ...

local getFirst = list[key]?[1];
--[[
eq:

local getFirst = nil;
do 
    local temp = list[key];
    if temp ~= nil then getFirst = temp[1]; end
end
]]

In the above cases, types that are null-conditional, i.e. T? will result in index<T, U>? when evaluated, and types with T would result in regular index<T, U>. This makes sense for the following example:

-- sealed table '{a:{b:string}}'
local t = {a = {b = "some text"}};

local text = t.a?.b; -- text: string
-- 't.a' is '{b:string}' and not 'nil' / nullable, so regular 'a.b' indexing is used: 'index<a, 'b'>'

local grandParent = part.Parent?.Parent; -- grandParent: Instance?
-- 'part.Parent' is 'Instance?' and not 'Instance', so null-conditional indexing is used, resulting in 'index<Instance, "Parent">?' or 'Instance??' aka 'Instance?'
-- in this case, 'part.Parent.Parent' by contrast will emit a 'Type Error' since 'index<Instance?, "Parent">' is invalid

Or, the following syntax for short circuit evaluations, inspired by Initializers in if statements:

local value = if local temp = somefunction() in temp ~= nil then temp else default();
-- or alternatively
local value = if local temp = somefunction() in temp == nil then default() else temp;

In both cases, somefunction() executes once only and default() one or no times, and would create a temp variable scoped to the if statement.

@surfbryce
Copy link

@Wunder-Wulfe Would probably be better that you make a RFC for this, probably paired with null assertion operators as well

@alexmccord
Copy link
Contributor

We already had the safe navigation operator RFC and we had to veto it out.

  1. RFC: Do not implement safe navigation operator luau#501
  2. RFC: Safe navigation operator luau#142

@deviaze
Copy link
Contributor

deviaze commented Jun 11, 2025

Not sure if it's ever been specifically considered, but we could adapt Rust's let else syntax, where the else branch must diverge. Maybe we don't have to make our else branches diverge like rust's if they evaluate to the same type (at typetime)? At runtime, if the expression isn't a return it just functions normally as a default value. In Luau it could be like:

local cat = animals_folder:FindFirstChild("Cat") else return nil
print(cat.Position) -- indexing .Position doesn't error because cat can't be nil

-- runtime equivalent to
local cat = animals_folder:FindFirstChild("Cat")
if cat == nil then
    return nil
end

-- as default value

local cat = animals_folder:FindFirstChild("Cat") else default_cat

-- equivalent to
local cat = animals_folder:FindFirstChild("Cat")
if cat == nil then
    cat = default_cat
end

@gaymeowing
Copy link
Contributor

Not sure if it's ever been specifically considered, but we could adapt Rust's let else syntax, where the else branch must diverge. Maybe we don't have to make our else branches diverge like rust's if they evaluate to the same type (at typetime)? At runtime, if the expression isn't a return it just functions normally as a default value. In Luau it could be like:

local cat = animals_folder:FindFirstChild("Cat") else return nil
print(cat.Position) -- indexing .Position doesn't error because cat can't be nil

-- runtime equivalent to
local cat = animals_folder:FindFirstChild("Cat")
if cat == nil then
    return nil
end

-- as default value

local cat = animals_folder:FindFirstChild("Cat") else default_cat

-- equivalent to
local cat = animals_folder:FindFirstChild("Cat")
if cat == nil then
    cat = default_cat
end

You've just described or

@deviaze
Copy link
Contributor

deviaze commented Jun 12, 2025

You've just described or

iirc or doesn't work there w/ multirets and also evals if the LHS is false (not just nil)

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.

10 participants