Reference

Diagnostics

Every error and warning the OSL compiler reports and every error kind a running program can throw, with what triggers each one and how to fix it.

A diagnostic is a message from the compiler about your code. Errors stop the build. Warnings point at code that compiles but is probably wrong, risky or hard to read. A third family, runtime errors, is not a diagnostic at all: it is what a running program throws, and what your code catches. This page lists all three, so you can look up a message by its name.

Reading a diagnostic#

Every diagnostic has a kind, a message and a place:

main.osl:2:9: osl: TypeError: Cannot store a string in int; convert it with .toInt(), which gives 0 for text that isn't a number, or with .tryAs<int>() to get a result you can check

   |
 2 | int n = "hello"
   |         ^^^^^^^
  • The kind ends in Error or Warning, and says what sort of problem it is.
  • The place is file:line:column, and the carets underline the code concerned.
  • A warning may add notes, and related places such as where a value was declared.
  • Every warning also has a code such as OSL-UNUSED-VARIABLE. It does not appear in the message. You use it to silence or reconfigure the warning.

Errors make osl run, osl compile and osl test fail with status 1 and build nothing. Warnings are printed, and the program still builds. A command prints at most 20 warnings and counts the rest, unless you pass --all-warnings. osl lsp check prints all of them.

A project can promote a warning to an error in opal.json. The warning's kind then changes its ending: an UnusedVariableWarning becomes an UnusedVariableError, with a note that says the project made it one. A warning with no kind of its own becomes a LintError.

The examples below that show errors are marked as failing on purpose, so the site displays the diagnostic the compiler gave. The warning examples compile, and show their warning.

Compile errors#

KindRaised when
SyntaxErrorThe code is not valid OSL.
TypeErrorA value does not have the type the code needs, or something is called that does not exist.
ReferenceErrorA name is not defined, not visible, or used before its declaration runs.
ImportErrorAn import cannot be found, read or used.
ExportErrorA module exports something it does not declare.
IndexErrorA constant index is outside the array, string or tuple.
MathErrorA constant division by zero.
OverflowErrorA constant integer calculation overflows.
ShiftErrorA shift by a negative constant.
ConcurrencyErrorAn atomic block does something that could deadlock.
ConfigErrorThe lint section of opal.json is invalid.
RecursionErrorA function calls itself without end, and the stack ran out. Add a base case.
BuildErrorThe compiler produced Go code that Go could not build. Report it, with the program, as a bug.
InternalErrorThe compiler used one of its own helpers without including it. Also a compiler bug.

SyntaxError#

The code does not parse. The message says what the compiler expected, and often the fix.

main.osl
log "a", "b"
Click the code to edit it

Values after log are separated by spaces, not commas. Other common causes:

  • A block opened with ( and never closed with ).
  • An operator with nothing on one side, such as log 1 +.
  • A string that is not closed.
  • A variable used without being declared (name = "a" instead of string name = "a").
  • A function body written after -> that is more than one value. Wrap statements in a block: def f() -> ( ... ).
  • export written before a declaration. Declare the name, then write export { name }.

TypeError#

By far the most common error. It covers a value of the wrong type, a call with the wrong arguments, an unknown method, writing to a const, and reading a private field.

main.osl
int n = "hello"
log n
Click the code to edit it

The message names both types and suggests the conversion. Fix it by converting (.toInt() gives 0 for text that is not a number, .tryAs<int>() gives a result you can check), or by declaring the right type.

Other TypeError messages, and what to do:

Message startsCauseFix
Argument 2 to add expects int, got stringA call passes the wrong type.Convert the argument.
Too many arguments in call to fA call passes more than the function takes.Remove the extra argument.
Unknown method 'nothing' on stringNo such method for that type.Check the name against the type's methods.
Cannot reassign const variable 'k'A const is assigned a second time.Declare it without const, or compute a new variable.
'_n' is a private field of CounterA name starting with _ is read from outside its type.Add a method that returns it.
Cannot assign nullable type int? to non-nullable variable of type intA value that may be null is stored where null is not allowed.Narrow it with .<int> or .<int>(fallback), or check for null first.
Cannot access property 'length' on array; did you mean .len?An old or foreign name.Use the name the message suggests.
Cannot use 200 as int8A constant does not fit the type.Use a wider type.

ReferenceError#

A name that is not defined at that point.

main.osl
log missing
Click the code to edit it

It also happens when a variable is declared inside a block and used outside it:

main.osl
int limit = 3
if limit > 1 (
  int inner = 1
  log inner
)
log inner
Click the code to edit it

Declare the variable before the block and assign it inside. A third form reports a top-level variable that is read before the line that declares it has run:

main.osl
log later
int later = 1
Click the code to edit it

Move the declaration above its first use. A message that ends add import "std:thread" means the name belongs to a standard package you have not imported.

ImportError#

An import the compiler cannot satisfy.

main.osl
import "std:nothing"
Click the code to edit it

The message may suggest a nearby package name (Did you mean 'std:fs'?), or say that a feature is built in: strings, arrays and the maths functions need no import. osl package lists the real names. Other causes:

  • A local file that does not exist, or cannot be read.
  • A path that climbs out of the project folder (local import escapes project root).
  • Two imports that bind the same name.
  • Two packages that cannot be used in one program because they define the same name.
  • Importing a Go package without the go: prefix.

A second file shows two more:

import {b} from "./lib.osl"
log b
Click the code to edit it

lib.osl exports only a, so importing b fails with module does not export 'b'. A private name is reported as 'b' is private to its module.

ExportError#

An export { ... } list that names something the file does not declare, or names the same thing twice.

import "./lib.osl"
log a
Click the code to edit it

The error points into lib.osl: exported name 'missing' is not declared. Declare the name, or take it out of the list.

IndexError#

OSL counts from 1. A constant index of 0 is always a mistake, and the compiler says so before the program runs. The same error covers a constant tuple index or array assignment outside the bounds.

main.osl
string[] items = ["a", "b"]
log items[0]
Click the code to edit it

Use items[1] for the first item and items[-1] for the last. An index that is not a constant, and is out of range at run time, does not stop the program: reading it gives null.

MathError, OverflowError and ShiftError#

Errors found by calculating with constants while compiling:

main.osl
log 10 / 0
Click the code to edit it
main.osl
int big = 9223372036854775807 + 1
log big
Click the code to edit it
main.osl
log 1 << -1
Click the code to edit it

MathError covers dividing by zero and remainders with zero. OverflowError covers a constant integer calculation that does not fit in an int (addition, negation, abs, a left shift). ShiftError is a shift count that is negative. A zero divisor that is only known at run time is not an error: dividing a number by zero gives infinity.

ConcurrencyError#

An atomic block takes the locks for every shared value it reaches before it runs, so it must not run other OSL code that could wait on those locks. The compiler rejects calling functions, creating functions, passing callbacks, and declaring things inside the block.

main.osl
int n = 0
def one() int -> 1
atomic (
  n = n + one()
)
log n
Click the code to edit it

Call the function before the block and use its result inside, or after it.

ConfigError#

A problem in opal.json, reported at its place in that file: an unknown lint code, a bad severity, or a bad option. It cannot be silenced, because it is in the configuration rather than in the code.

opal.json:1:102: osl: ConfigError: unknown lint rule "OSL-BOGUS" in lint.rules

Warnings#

Warnings are grouped here by what they are about. Each group lists the kind, the code, what triggers it and how to fix it, with an example that shows the warning. The full list of codes is in the table at the end.

Unused code#

UnusedVariableWarning (OSL-UNUSED-VARIABLE) is reported for a variable, loop variable or loop index that is declared and never read. Name it _, or start the name with an underscore, to say it is unused on purpose.

main.osl
int count = 3
Click the code to edit it
main.osl
for i in 1 to 3 (
  log "hello"
)
Click the code to edit it

For the loop, use for _ in 1 to 3. Unused function parameters are not reported.

UnusedValueWarning (OSL-UNUSED-VALUE) is reported for a value on a line of its own that does nothing. Remove it, or write void before it to drop the value on purpose. osl fix adds the void.

main.osl
int a = 1
a + 1
log a
Click the code to edit it

UnusedImportWarning (OSL-UNUSED-IMPORT) is reported for a go: import that nothing uses. Remove the line.

main.osl
import "go:strings"
log "no strings used"
Click the code to edit it

Failure that nobody handles#

ThrowWarning (OSL-UNHANDLED-THROW) is reported when a call that can fail is used as a plain value, so a failure would crash the program. The message names the condition that would make the call safe, lists the call chain, and names the error kinds the call can fail with.

main.osl
import "std:fs"

string text = fs.readFile("data.txt")
log text
Click the code to edit it

Handle the result with catch, switch or !, keep it as a result, or unwrap() it on purpose. A guard also removes the warning: after if !fs.isFile(path) return, the compiler knows fs.readFile(path) will not fail for that reason. See Errors.

main.osl
import "std:fs"

def load() string (
  string text = fs.readFile("data.txt") catch (e) (
    return "could not read: " ++ e.kind
  )
  return text
)

log load()
Click the code to edit it

Explicit unwrap, unwrapErr, expect, throw and postfix ! do not warn, because they are decisions.

Types and assertions#

TypeWarning (OSL-TYPEOF-KNOWN, OSL-TYPEOF-NULLABLE) is reported when typeof is asked about a value whose type the compiler already knows. typeof is for values that can be more than one type. Use the type directly, or compare with null for an optional value.

main.osl
int a = 1
log typeof(a)
Click the code to edit it

AssertionWarning is reported for .<T> assertions that do nothing or cannot work:

CodeReported whenFix
OSL-REDUNDANT-ASSERTThe value is already known to be T.Remove the assertion. osl fix does.
OSL-KNOWN-ASSERT-ELSE.<T>(fallback) is applied to a type that is never T, so it always gives the fallback.Convert with .as<T> instead.
OSL-CHAINED-ASSERTTwo assertions in a row.Assert once, against the final type.
OSL-NARROWING-ASSERTA known type is asserted as another, which can hide invalid values.Convert explicitly, or validate first.
main.osl
int a = 1
log a.<int>
Click the code to edit it

ConversionWarning (OSL-NEEDLESS-TRYAS) is reported for .tryAs<T>() on a value that already is T and so cannot fail. Use the value as it is.

main.osl
int a = 1
log a.tryAs<int>().unwrap()
Click the code to edit it

BoundaryWarning is about any and unknown, which hide where input should be checked:

CodeReported when
OSL-LOOSE-PARAMA function declares an any parameter.
OSL-LOOSE-RETURNA function returns any.
OSL-LOOSE-ALIASA type alias is any, or hides a loose parameter or return.
OSL-LOOSE-DICTIONARYA dictionary type has any values.
OSL-WIDENED-VALUEA known value is stored as any and narrowed again with .<T>.
OSL-REFLECT-IMPORTThe program imports go:reflect.
main.osl
def show(any value) (
  log value
)

show(1)
Click the code to edit it

Declare the real type, or decode unknown input at the edge of your program: a parameter whose first use is a typeof check or a schema parse/isValid is treated as that edge and does not warn. A function whose job is to take anything, like a logger, can silence the warning with a comment.

NullableWarning (OSL-NULLABLE-UNION) is reported for a union that lists null as a member. Write it with the ? suffix.

main.osl
(string | null) name = "a"
log name
Click the code to edit it

SafetyWarning (OSL-RAW-WITHOUT-COMMENT) is reported for a raw() call, which emits Go verbatim and skips every check, when no // comment above it says why it is needed.

Conditions and comparisons#

ConditionWarning (OSL-CONSTANT-CONDITION) is reported for an if or while whose condition is always the same, and for a comparison of two strings known at compile time. Remove the if, or use a variable.

main.osl
int x = 3
if true (
  log x
)
Click the code to edit it

ComparisonWarning covers comparisons that probably do not mean what they say:

CodeReported whenFix
OSL-SELF-COMPARISONA value is compared with itself. NaN is the one value unequal to itself, so use math.isNan to test for it.Compare the intended operands.
OSL-MIXED-KIND-EQUALITY== compares different kinds, such as an int and a string, which compares their text.Convert one side, with .toStr() or .tryAs<int>().
OSL-POINTER-EQUALITY== on two handles, which compares their contents.Use === to compare identity.
main.osl
int a = 1
string s = "1"
log a == s
Click the code to edit it

NegationWarning (OSL-DOUBLE-NEGATION) is reported for !!value. Use the value, or .toBool() to convert it.

PrecedenceWarning (OSL-MIXED-OPERATORS, OSL-MIXED-LOGICAL-OPERATORS) is reported when different arithmetic operators, or different logical operators, appear together without parentheses. OSL evaluates arithmetic strictly left to right, so a + b * c is (a + b) * c. Add parentheses to show which you mean.

main.osl
int a = 1
int b = 2
int c = 3
log a + b * c
Click the code to edit it

AssignmentWarning (OSL-SELF-ASSIGNMENT) is reported for a = a. Remove it.

Readability#

ReadabilityWarning is reported for code that has a simpler equivalent. Most carry an exact replacement, so the editor offers it as a quick fix and osl fix applies it.

CodePatternWrite instead
OSL-NEGATED-EQUALITY!(a == b)a != b
OSL-REDUNDANT-TERNARYif ready then true else falseready
OSL-LEGACY-TERNARYready ? "go" "wait"if ready then "go" else "wait"
OSL-NESTED-TERNARYA conditional inside another conditionalAn else if chain, or match
OSL-REDUNDANT-COALESCEvalue ?? nullvalue
OSL-PREFIX-STRIPif s.startsWith(p) ( s = s.replace(p, "") )s.stripStart(p)
OSL-TUPLE-UNION([string] | [int])[][string | int][]
main.osl
int a = 1
log !(a == 2)
Click the code to edit it
main.osl
boolean ok = true
log ok ? "yes" "no"
Click the code to edit it

BranchWarning is reported for an if that says less than it could:

CodeReported whenFix
OSL-BOOLEAN-BRANCHESThe branches only return true and return false.Return the condition.
OSL-IDENTICAL-BRANCHESTwo branches run the same code.Merge them, or drop the condition.
OSL-INVERTED-CONDITIONAn if is followed by an if on the negated condition.Use else.
main.osl
def isBig(int n) boolean (
  if n > 10 ( return true ) else ( return false )
)

log isBig(3)
Click the code to edit it

RedundantWarning (OSL-REDUNDANT-CALL) is reported for a call with no effect, such as .iter() on an iterator. Remove it.

MethodWarning is reported for a method call that does something other than it seems:

  • OSL-STRING-TO-ARRAY: toArray() on a string gives an array holding the string. Use .toArr() for its characters.
  • OSL-OBJECT-MEMBER-CALL: objects have no method of that name, so the call reads the object's member of that name. The message names the method you probably meant.
main.osl
log "abc".toArray()
Click the code to edit it

Changing values#

Warning (OSL-DELETE-IN-PLACE) is reported when the result of delete on an array or object is stored under a second name. delete changes the value in place and returns it, so both names now point at the same changed value. Use array.clone().delete(...) or object.omit(...) for a changed copy.

main.osl
int[] a = [1, 2, 3]
int[] b = a.delete(1)
log a b
Click the code to edit it

StructWarning (OSL-LOST-STRUCT-CHANGE) is reported when code changes a copy of a struct that it then throws away: a function that modifies its own copy of a struct parameter, or a loop that modifies the loop's copy of an item. Take a pointer, return the changed value, change the item by index, or make the type a class.

main.osl
struct Point (
  int x = 0
)

Point[] points = [Point(1)]
for p in points (
  p.x = 5
)
log points[1].x
Click the code to edit it

StructCopyNote (OSL-STRUCT-COPY) is a note rather than a warning. It marks code where a type that used to be shared became a struct, so assignment now copies. Check that a copy is what the code needs.

Concurrency and cleanup#

ConcurrencyWarning has two codes:

  • OSL-ATOMIC-BLOCKING: a call inside an atomic block waits (sleeping, reading input, waiting on a thread) while holding the block's locks, so every thread that needs those values waits too. Do the slow thing before or after the block.
  • OSL-CONCURRENCY-FOREIGN-ESCAPE: a shared OSL array or object is passed to a Go package function. Go code can keep the pointer or start its own goroutines, outside OSL's automatic locking. Copy the data first, or keep the synchronisation inside the Go call.
main.osl
import "go:slices"

int[] nums = [3, 1, 2]
slices.Sort(nums)
log nums
Click the code to edit it

DeferWarning (OSL-DEFER-IN-LOOP) is reported for a defer inside a loop. A defer runs when its function (or the program) ends, not at the end of each loop pass. Move the loop body into a function and call it from the loop.

main.osl
for i in 1 to 3 (
  defer log i
)
Click the code to edit it

Deprecation#

DeprecationWarning (OSL-DEPRECATED, OSL-DEPRECATED-IMPORT) is reported for a function, method, option, constant or import that still works but has a replacement. The message names the replacement. osl/name imports are the old spelling of std:name.

main.osl
import "std:fs"

log fs.getBase("docs/page.md")
Click the code to edit it

Each package page marks its deprecated members, and the API reference says what replaces them.

Pedantic and opt-in rules#

OSL-IMPLICIT-NARROW is pedantic: it is reported only with --all-warnings, or when a project sets it to warning. Four more rules are off until a project turns them on in opal.json, and report as these kinds: OSL-COMMENT as CommentWarning, OSL-LONG-FUNCTION as LengthWarning, OSL-UNTYPED-GLOBAL as TypeWarning, and OSL-OBJECT-PARAM as BoundaryWarning. See Lint settings.

All warning codes#

CodeKindGroup
OSL-ATOMIC-BLOCKINGConcurrencyWarningConcurrency
OSL-BOOLEAN-BRANCHESBranchWarningReadability
OSL-CHAINED-ASSERTAssertionWarningTypes
OSL-COMMENT (opt in)CommentWarningOpt in
OSL-CONCURRENCY-FOREIGN-ESCAPEConcurrencyWarningConcurrency
OSL-CONSTANT-CONDITIONConditionWarningConditions
OSL-DEFER-IN-LOOPDeferWarningConcurrency
OSL-DELETE-IN-PLACEWarningValues
OSL-DEPRECATEDDeprecationWarningDeprecation
OSL-DEPRECATED-IMPORTDeprecationWarningDeprecation
OSL-DOUBLE-NEGATIONNegationWarningConditions
OSL-IDENTICAL-BRANCHESBranchWarningReadability
OSL-IMPLICIT-NARROW (pedantic)WarningTypes
OSL-INVERTED-CONDITIONBranchWarningReadability
OSL-KNOWN-ASSERT-ELSEAssertionWarningTypes
OSL-LEGACY-TERNARYReadabilityWarningReadability
OSL-LONG-FUNCTION (opt in)LengthWarningOpt in
OSL-LOOSE-ALIASBoundaryWarningTypes
OSL-LOOSE-DICTIONARYBoundaryWarningTypes
OSL-LOOSE-PARAMBoundaryWarningTypes
OSL-LOOSE-RETURNBoundaryWarningTypes
OSL-LOST-STRUCT-CHANGEStructWarningValues
OSL-MIXED-KIND-EQUALITYComparisonWarningConditions
OSL-MIXED-LOGICAL-OPERATORSPrecedenceWarningConditions
OSL-MIXED-OPERATORSPrecedenceWarningConditions
OSL-NARROWING-ASSERTAssertionWarningTypes
OSL-NEEDLESS-TRYASConversionWarningTypes
OSL-NEGATED-EQUALITYReadabilityWarningReadability
OSL-NESTED-TERNARYReadabilityWarningReadability
OSL-NULLABLE-UNIONNullableWarningTypes
OSL-OBJECT-MEMBER-CALLMethodWarningReadability
OSL-OBJECT-PARAM (opt in)BoundaryWarningOpt in
OSL-POINTER-EQUALITYComparisonWarningConditions
OSL-PREFIX-STRIPReadabilityWarningReadability
OSL-RAW-WITHOUT-COMMENTSafetyWarningTypes
OSL-REDUNDANT-ASSERTAssertionWarningTypes
OSL-REDUNDANT-CALLRedundantWarningReadability
OSL-REDUNDANT-COALESCEReadabilityWarningReadability
OSL-REDUNDANT-TERNARYReadabilityWarningReadability
OSL-REFLECT-IMPORTBoundaryWarningTypes
OSL-SELF-ASSIGNMENTAssignmentWarningConditions
OSL-SELF-COMPARISONComparisonWarningConditions
OSL-STRING-TO-ARRAYMethodWarningReadability
OSL-STRUCT-COPYStructCopyNoteValues
OSL-TUPLE-UNIONReadabilityWarningReadability
OSL-TYPEOF-KNOWNTypeWarningTypes
OSL-TYPEOF-NULLABLETypeWarningTypes
OSL-UNHANDLED-THROWThrowWarningFailure
OSL-UNTYPED-GLOBAL (opt in)TypeWarningOpt in
OSL-UNUSED-IMPORTUnusedImportWarningUnused code
OSL-UNUSED-VALUEUnusedValueWarningUnused code
OSL-UNUSED-VARIABLEUnusedVariableWarningUnused code
OSL-WIDENED-VALUEBoundaryWarningTypes

The compile errors have codes too (OSL-SYNTAX, OSL-TYPE, OSL-IMPORT, OSL-REFERENCE, OSL-ZERO-INDEX, OSL-COMPILE), but a project cannot change them: only warnings are configurable.

Runtime errors#

A compile error stops the build. A runtime error is thrown while the program runs. It carries a kind and a message, and the program catches it with catch, try or a result. If nothing catches it, the program prints the error with the line it came from and exits with status 1.

main.osl
import "std:fs"
import "std:json"

auto missing = fs.readFile("missing.txt")
auto notFile = fs.readFile(".")
auto bad = json.parse<int[]>("not json")
auto count = "abc".tryAs<int>()
log missing.unwrapErr().kind
log notFile.unwrapErr().kind
log bad.unwrapErr().kind
log count.unwrapErr().kind
log missing.unwrapErr().message
log bad.unwrapErr().message
Click the code to edit it

The message starts with the function that failed. The kind is the part to test, because it stays the same when the wording changes:

main.osl
import "std:fs"

fs.readFile("missing.txt") catch (e) (
  log "caught " ++ e.kind
)
Click the code to edit it

Your own code throws an error with throw error("Kind: message"). Text that starts with a kind and a colon takes that kind, and plain text becomes a RuntimeError:

main.osl
def setSize(int size) (
  if size <= 0 (
    throw error("ValueError: size must be positive")
  )
)

auto r = try(setSize(0))
log r.unwrapErr().kind
log r.unwrapErr().message
Click the code to edit it

Kinds#

These are the kinds the standard packages throw, and the Go errors they translate. A package function's documentation names the kinds it can fail with.

KindMeaningTypical cause
NotFoundErrorSomething that was asked for does not exist.A missing file or folder, an unknown key.
PermissionErrorThe operating system refused.Reading a file you may not read.
IOErrorReading or writing failed for another reason.A path that is a folder, a closed pipe, end of input.
ParseErrorText is not in the expected format.Bad JSON, a number that is not one, a malformed URL, a bad regular expression, a date that does not match.
ValueErrorAn argument is outside what a function accepts.A negative size, a probability above 1, NaN in JSON.
TypeErrorA value has the wrong type at run time..<int> applied to a string, a value JSON cannot represent.
StateErrorAn operation is not allowed in the object's current state.Using something after closing it, unwrapErr() on a result that is ok.
TimeoutErrorA deadline passed.A request or a wait with a timeout.
NetworkErrorThe network failed.A connection refused, a TLS or DNS failure.
UnsupportedErrorThe system or the function cannot do this.Desktop notifications on a platform without them.
AssertionErrorA check from std:testing failed.A failed testing.equal, or a failed check in a test block.
RuntimeErrorAny other failure, and the kind of text thrown without a kind.throw error("something broke").

A Go error that comes from a Go package is given a kind from its type: a timeout becomes TimeoutError, a missing file NotFoundError, a permission problem PermissionError, a network error NetworkError, a parse error ParseError, and anything unrecognised RuntimeError.

Fixing a problem you do not understand#

  1. Read the first line of the message to the end. Most messages name the fix.
  2. Look at the line and the underlined span, not only the line number: the compiler points at the exact value.
  3. Fix the first error first. Later errors are often caused by it, and the compiler reports up to 20 at once.
  4. Run osl lsp check file.osl to see every diagnostic of a file and the files it imports, including warnings.
  5. If a warning is wrong for your code, silence that one line with a comment rather than turning the rule off for the project.