Skip to content

Commit

Permalink
Add result and spicy::Error types to Spicy to facilitate error ha…
Browse files Browse the repository at this point in the history
…ndling.

The `result` and `error` types were already implemented internally
HILTI-side, but not yet available to Spicy users. This exposes them to
Spicy as well. To avoid name clashes with existing code, we don't
introduce `error` as a new type keyword, but instead make it available
as a library type ``spicy::Error``.

Typical usage is something like this:

```
function foo() : result<int64> {
    ...
    if ( everything_is_ok )
        return 42;
    else
        return error"Something went wrong.";
}

if ( local x = foo() )
    print "result: %d " % *x;
else
    print "error: %s " % x.error();
```

The documentation has more specifics.

Closes #90.
Closes #1733.
  • Loading branch information
rsmmr committed Jun 12, 2024
1 parent ea47943 commit 529521f
Show file tree
Hide file tree
Showing 18 changed files with 226 additions and 100 deletions.
2 changes: 2 additions & 0 deletions doc/autogen/reserved-keywords.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ False
None
Null
True
__error
__library_type
add
addr
Expand Down Expand Up @@ -70,6 +71,7 @@ public
real
regexp
reject
result
return
set
sink
Expand Down
8 changes: 6 additions & 2 deletions doc/autogen/types/error.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@

.. rubric:: Operators

.. spicy:operator:: error::Call error error(string)
.. spicy:operator:: error::Equal bool t:error <sp> op:== <sp> t:error
Creates an error with the given message.
Compares two error descriptions lexicographically.

.. spicy:operator:: error::Unequal bool t:error <sp> op:!= <sp> t:error
Compares two error descriptions lexicographically.

4 changes: 2 additions & 2 deletions doc/autogen/types/result.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@

.. rubric:: Operators

.. spicy:operator:: result::Deref <dereferenced~type> op:* t:result op:
.. spicy:operator:: result::Deref <type~of~stored~value> op:* t:result op:
Retrieves value stored inside the result instance. Will throw a
Retrieves the value stored inside the result instance. Will throw a
``NoResult`` exception if the result is in an error state.

64 changes: 64 additions & 0 deletions doc/programming/language/types.rst
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,26 @@ Enum types associate labels with numerical values.

.. include:: /autogen/types/enum.rst

.. _type_error:

spicy::Error
------------

``spicy::Error`` captures an error message. It's primarily meant for
use with the :ref:`result\<T\> <type_result>` type; see there for
more.

.. rubric:: Type

- ``spicy::Error`` (note that you need to ``import spicy`` to use this)

.. rubric:: Constants

- ``error"MSG"`` creates a value of type ``spicy::Error`` capturing the
error message ``MSG``.

.. include:: /autogen/types/error.rst

.. _type_exception:

Exception
Expand Down Expand Up @@ -308,6 +328,50 @@ This type supports the :ref:`pack/unpack operators <packing>`.

.. include:: /autogen/types/real.rst

.. _type_result:

Result
------

A ``result<T>`` is a type facilitating error handling by holding
either a value of type ``T`` or an error message. It's most useful
when used as the return value of a function that would normally produce
a computed value of some kind, but may fail doing so. Typical example:

.. spicy-code::

function compute_value() : result<int64> {

local value: int64;

[... Try to compute value ...]

if ( everything_went_ok )
return value;
else
return error"Something went wrong.";
}

if ( local x = compute_value() )
print "result: %d " % *x;
else
print "error: %s " % x.error();
.. **
As you can see, the ``result<int64>`` return value of
``compute_value()`` can be set from either a corresponding integer or
an appropriate error message. In the latter case, ``error"MSG"``
instantiates an error value of type :ref:`type_error`. As the ``if``
statements shows, ``result<T>`` coerces to a boolean value depending
on whether it holds a value or an error.

.. rubric:: Type

- ``result<TYPE>``

.. include:: /autogen/types/result.rst

.. _type_reference:

Reference
Expand Down
2 changes: 1 addition & 1 deletion doc/scripts/autogen-docs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ SPICY_TYPES=address,bitfield,bool,bytes,enum,exception,integer,interval,list,map
# Create list of reserved keywords.
cat ${ROOTDIR}/spicy/toolchain/src/compiler/parser/scanner.ll \
| grep "return.*token::" \
| grep '^[a-zA-Z_]\{2,\}' \
| grep '^[a-zA-Z_]\{2,\}[^{}]* ' \
| awk '{print $1}' \
| sort >${AUTOGEN_STAGE}/reserved-keywords.txt

Expand Down
1 change: 1 addition & 0 deletions hilti/toolchain/src/ast/operators/error.cc
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ class Ctor : public Operator {
.result = {Constness::Const, builder->typeError()},
.ns = "error",
.doc = "Creates an error with the given message.",
.skip_doc = true, // not available in Spicy source code
};
}

Expand Down
4 changes: 2 additions & 2 deletions hilti/toolchain/src/ast/operators/result.cc
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@ class Deref : public Operator {
return Signature{
.kind = Kind::Deref,
.op0 = {parameter::Kind::In, builder->typeResult(type::Wildcard())},
.result_doc = "<dereferenced type>",
.result_doc = "<type of stored value>",
.ns = "result",
.doc =
"Retrieves value stored inside the result instance. Will throw a ``NoResult`` exception if the "
"Retrieves the value stored inside the result instance. Will throw a ``NoResult`` exception if the "
"result is in an error state.",
};
}
Expand Down
4 changes: 4 additions & 0 deletions spicy/lib/spicy.spicy
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ public type DecodeErrorStrategy = enum {
STRICT # runtime error is triggered
} &cxxname="hilti::rt::bytes::DecodeErrorStrategy";

## Represents the error value of ``result<T>`` instances. Use ``error"My error message"``
## to create an error value of this type.
public type Error = __error;

## Captures state for incremental regular expression matching.
public type MatchState = struct {
} &cxxname="hilti::rt::regexp::MatchState";
Expand Down
12 changes: 10 additions & 2 deletions spicy/toolchain/src/compiler/parser/parser.yy
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ namespace spicy { namespace detail { class Parser; } }
%verbose

%glr-parser
%expect 108
%expect-rr 164
%expect 111
%expect-rr 168

%{

Expand Down Expand Up @@ -131,6 +131,7 @@ static std::vector<hilti::DocString> _docs;
%token <std::string> PREPROCESSOR "preprocessor directive"
%token <std::string> CSTRING "string value"
%token <std::string> CBYTES "bytes value"
%token <std::string> CERROR "error value"
%token <std::string> CREGEXP "regular expression value"
%token <std::string> CADDRESS "address value"
%token <std::string> CPORT "port value"
Expand Down Expand Up @@ -174,6 +175,7 @@ static std::vector<hilti::DocString> _docs;
%token END_
%token ENUM
%token EQ
%token __ERROR
%token EXCEPTION
%token EXPORT
%token FILE
Expand Down Expand Up @@ -230,6 +232,7 @@ static std::vector<hilti::DocString> _docs;
%token PRIVATE
%token PUBLIC
%token REGEXP
%token RESULT
%token RETURN
%token SET
%token SHIFTLEFT
Expand Down Expand Up @@ -605,6 +608,7 @@ base_type_no_ref
| ADDRESS { $$ = builder->typeAddress(__loc__); }
| BOOL { $$ = builder->typeBool(__loc__); }
| BYTES { $$ = builder->typeBytes(__loc__); }
| __ERROR { $$ = builder->typeError(__loc__); }
| INTERVAL { $$ = builder->typeInterval(__loc__); }
| NETWORK { $$ = builder->typeNetwork(__loc__); }
| PORT { $$ = builder->typePort(__loc__); }
Expand All @@ -627,6 +631,7 @@ base_type_no_ref
| CONST_ITERATOR type_param_begin qtype type_param_end { $$ = iteratorForType(builder, std::move($3), __loc__)->type(); }
| ITERATOR type_param_begin qtype type_param_end { $$ = iteratorForType(builder, std::move($3), __loc__)->type(); }
| OPTIONAL type_param_begin qtype type_param_end { $$ = builder->typeOptional($3, __loc__); }
| RESULT type_param_begin qtype type_param_end { $$ = builder->typeResult($3, __loc__); }
| VIEW type_param_begin qtype type_param_end { $$ = viewForType(builder, std::move($3), __loc__)->type(); }

| MAP type_param_begin qtype ',' qtype type_param_end { $$ = builder->typeMap(std::move($3), std::move($5), __loc__); }
Expand Down Expand Up @@ -1036,6 +1041,7 @@ ctor : CADDRESS { $$ = builder->ctorAddress(hil
| CADDRESS '/' CUINTEGER { $$ = builder->ctorNetwork(hilti::rt::Network($1, $3), __loc__); }
| CBOOL { $$ = builder->ctorBool($1, __loc__); }
| CBYTES { $$ = builder->ctorBytes(std::move($1), __loc__); }
| CERROR { $$ = builder->ctorError(std::move($1), __loc__); }
| CPORT { $$ = builder->ctorPort(hilti::rt::Port($1), __loc__); }
| CNULL { $$ = builder->ctorNull(__loc__); }
| CSTRING { $$ = builder->ctorString($1, false, __loc__); }
Expand All @@ -1052,6 +1058,8 @@ ctor : CADDRESS { $$ = builder->ctorAddress(hil
But not sure if that'd change much so leaving here for now.
*/
| OPTIONAL '(' expr ')' { $$ = builder->ctorOptional(std::move($3), __loc__); }
| RESULT '(' expr ')' { $$ = builder->ctorResult(std::move($3), __loc__); }

| list { $$ = std::move($1); }
| map { $$ = std::move($1); }
| regexp { $$ = std::move($1); }
Expand Down
3 changes: 3 additions & 0 deletions spicy/toolchain/src/compiler/parser/scanner.ll
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ delete return token::DELETE;
else return token::ELSE;
end return token::END_;
enum return token::ENUM;
__error return token::__ERROR;
exception return token::EXCEPTION;
export return token::EXPORT;
file return token::FILE;
Expand Down Expand Up @@ -166,6 +167,7 @@ property return token::PROPERTY;
public return token::PUBLIC;
real return token::REAL;
regexp return token::REGEXP;
result return token::RESULT;
return return token::RETURN;
set return token::SET;
sink return token::SINK;
Expand Down Expand Up @@ -237,6 +239,7 @@ Null return token::CNULL;
{digits}|0x{hexs} yylval->build(hilti::util::charsToUInt64(yytext, 0, range_error_int)); return token::CUINTEGER;
{string} yylval->build(expandEscapes(driver, std::string(yytext, 1, strlen(yytext) - 2), *yylloc)); return token::CSTRING;
b{string} yylval->build(expandEscapes(driver, std::string(yytext, 2, strlen(yytext) - 3), *yylloc)); return token::CBYTES;
error{string} yylval->build(expandEscapes(driver, std::string(yytext, 6, strlen(yytext) - 7), *yylloc)); return token::CERROR;
'.' yylval->build(static_cast<uint64_t>(*(yytext +1))); return token::CUINTEGER;

{decfloat}|{hexfloat} yylval->build(hilti::util::charsToDouble(yytext, range_error_real)); return token::CUREAL;
Expand Down
20 changes: 10 additions & 10 deletions tests/Baseline/hilti.ast.basic-module/debug.log
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,16 @@
[debug/compiler] [HILTI] building scopes
[debug/compiler] [HILTI] resolving AST
[debug/compiler] -> modified
[debug/ast-stats] garbage collected 514 nodes in 12 rounds, 5166 left retained
[debug/ast-stats] garbage collected 514 nodes in 12 rounds, 5184 left retained
[debug/compiler] processing ASTs, round 2
[debug/compiler] [HILTI] building scopes
[debug/compiler] [HILTI] resolving AST
[debug/compiler] -> modified
[debug/ast-stats] garbage collected 205 nodes in 9 rounds, 5422 left retained
[debug/ast-stats] garbage collected 205 nodes in 9 rounds, 5440 left retained
[debug/compiler] processing ASTs, round 3
[debug/compiler] [HILTI] building scopes
[debug/compiler] [HILTI] resolving AST
[debug/ast-stats] garbage collected 29 nodes in 2 rounds, 5450 left retained
[debug/ast-stats] garbage collected 29 nodes in 2 rounds, 5468 left retained
[debug/ast-final] # [HILTI] Final AST (round 3)
[debug/ast-final] - ASTRoot [no parent] (<root>) [@a:XXX]
[debug/ast-final] | Foo -> declaration::Module <canonical-id="Foo" declaration="D2" dependencies="" ext=".hlt" fqid="Foo" id="Foo" linkage="public" path="<...>/basic-module.hlt" scope="" skip-implementation="false"> [parent @a:XXX] [@d:XXX] ([@d:XXX])
Expand Down Expand Up @@ -936,23 +936,23 @@
[debug/ast-stats] - # context types: 15
[debug/ast-stats] - # context modules: 2
[debug/ast-stats] - # nodes reachable in AST: 674
[debug/ast-stats] - # nodes live: 5450
[debug/ast-stats] - # nodes retained: 5450
[debug/ast-stats] - # nodes live: 5468
[debug/ast-stats] - # nodes retained: 5468
[debug/ast-stats] - # nodes live > 1%:
[debug/ast-stats] - AttributeSet: 58
[debug/ast-stats] - QualifiedType: 1909
[debug/ast-stats] - QualifiedType: 1915
[debug/ast-stats] - expression::Ctor: 68
[debug/ast-stats] - type::Bool: 138
[debug/ast-stats] - type::Bool: 140
[debug/ast-stats] - type::Bytes: 79
[debug/ast-stats] - type::Member: 113
[debug/ast-stats] - type::OperandList: 486
[debug/ast-stats] - type::OperandList: 488
[debug/ast-stats] - type::Real: 59
[debug/ast-stats] - type::SignedInteger: 78
[debug/ast-stats] - type::String: 66
[debug/ast-stats] - type::Unknown: 116
[debug/ast-stats] - type::UnsignedInteger: 332
[debug/ast-stats] - type::bytes::Iterator: 107
[debug/ast-stats] - type::operand_list::Operand: 879
[debug/ast-stats] - type::operand_list::Operand: 883
[debug/ast-stats] - type::stream::Iterator: 80
[debug/compiler] finalized AST
[debug/compiler] [HILTI] validating (post)
Expand All @@ -961,4 +961,4 @@
[debug/compiler] codegen module Foo to C++
[debug/compiler] generating C++ for module Foo
[debug/compiler] finalizing module Foo
[debug/ast-stats] garbage collected 5451 nodes in 17 rounds, 0 left retained
[debug/ast-stats] garbage collected 5469 nodes in 17 rounds, 0 left retained
Loading

0 comments on commit 529521f

Please sign in to comment.