Introduction

Refingere is a statically typed language made for web development first. A server, the app that runs in the browser, and the code they share are written in one language, checked by one compiler, and tested the same way - and the same language works just as well for command-line tools and everything else. It tries to stay out of your way, with a small number of ideas used everywhere: values instead of exceptions, a context instead of globals and hand-threaded parameters, and attributes instead of frameworks. It also comes with the things most programs end up needing - tables, graphs, vectors, dates, regular expressions, HTTP - so a program rarely starts by choosing libraries.

This page is a quick tour of the whole language. Each part links to the page that covers it in depth, and to an example that uses it for real.

A first program

A few things are already visible here:

  • No imports. Every file in a folder sees every other file's definitions, and the builtin types are always there. Another folder's global definitions are reached through a path in backticks.
  • No global I/O functions. Output goes to &stdout, and the arguments come from &args. Names starting with & are context values, and the test replaces both with inject - no mocking library, no parameters added for testing.
  • Attributes say what a function is for. @main is the program's entry point and @test marks a test, which fails when it ends in an error. See the Attributes reference.

Values and names

let introduces a name and set changes it, so every change is visible. ≔ (or :=) binds with an inferred type, : Type ≔ with a written one. Giving something a value is always ≔ - defaults, named arguments and fields too - so = is free to mean equality. The numeric types are exact about their size - U8 to U256, S8 to S256, F16, F32 and F64 - with Complex, Quaternion, Vector and Matrix alongside. The Types reference lists them all, with every method.

Functions and lambdas

Parameters can have defaults and be passed by name. Lambdas are written λ(x) ... (or lambda(x) ...), and the collections take them everywhere: map, filter, groupBy, sortedBy. A function whose body contains yield is a generator, returning a lazy Iterator - see Generators.

Control flow

if always has then, and its branches are a single expression or a block. for takes while, until, if and skipping modifiers, and break/continue name the loop by its variable. match tests values, types, struct fields, enum cases, tuples and errors, with an optional if guard. The exact grammar of each is in the Syntax reference.

Types

Structs hold named fields, enums are one of several cases - each empty, with positional values, or with fields - and interfaces list what a type promises, given by an implement block written separately from the type. Functions, structs and interfaces can be generic, and a constraint like T: HasArea says what the body can use. See Interfaces and Generics.

Types are built from parts instead of inherited. && combines structs' fields and || combines enums' cases - naming a struct there makes a case carrying its fields. The single operators are different: in a type, A & B is a tuple and A | B an untagged union, with no cases of its own - its members are told apart by their type. See Types.

Errors are values

There are no exceptions. A function that can fail says so in its type - Error ! U8 is an error or a U8 - and the caller decides what to do, with a few operators: !! passes the error up, !? replaces it, and match takes it apart - while ?? and ?! do the same for a value that's merely undefined. error message with value attaches data to an error. See Errors.

The context

A context value is read with &name, and an inject provides one for everything called from the rest of its block - however deep. The builtin ones cover everything a program touches outside itself: &stdout, &stderr, &stdin, &fs, &env, &args, &clock, &random, &logger, &heapAllocator, &panicHandler - and plain values such as &hashSeed, what every hash starts from.

That's what makes code testable without changing it: a test injects a MemoryFileSystem, a FixedClock, a SeededRandom or a TrackingAllocator, and the code under test never knows. Your own dependencies work the same way, injected for a file, a folder or the whole project with @inject, @injectLocally or @injectGlobally. See Context and Word Count.

Syntax you can extend

Math notation works as written: √, superscript powers and subscript indexing - see Numbers.

Other languages are embedded as macros - #SQL, #HTML, #CSS, #WGSL, #GLSL, #HLSL, #MSL, #SPIRV, #SVG, #JSON, #YAML, #TOML, #CSV, #REGEX, #MARKDOWN, #SHELL, #VUE - each checked by its own parser when the program compiles. Values go in through #{ }, always escaped the way that language needs, so injection attacks can't happen by construction. #comptime(...) moves work to compile time - see Compile-time Code.

Servers and browser apps

The same functions and structs become a server or a user interface through attributes: @serve and @get/@post/... turn functions into HTTP routes, and @app, @component and @state build an app that runs in the browser. Either way, the code stays ordinary functions and structs, so tests call them directly. See HTTP Server and Webapp.

This is what Refingere is built for first. Programs compile to WebAssembly, which every browser runs, and WASI runtimes such as wasmtime run the same modules on a server or the command line - see Compiling to WebAssembly. They compile to JavaScript too, as an ES module - see Compiling to JavaScript.

Where to go next

  • Getting Started - install the compiler, then run and test a first program.
  • The Language - each part of this tour in depth, one page after another, from syntax to files and folders.
  • Guides - testing, servers and browser apps, JavaScript, the GPU, and more.
  • Examples - complete programs with tests, from a Markdown parser to a game.
  • Reference - every construct, type, function, operator, attribute and context value, for looking things up.