npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

ranger-compiler

v4.0.2

Published

Ranger cross-language compiler - compile once, run anywhere

Readme

Ranger cross language compiler

Version 4.0.1 | Status: experimental

Licensing: Ranger-authored compiler and language sources are MIT licensed, unless a file says otherwise. Ranger-authored applications and technology under gallery/, including EVG and the Office document stack, are licensed under AGPL-3.0-or-later unless a file says otherwise. Third-party files keep their own licenses. See LICENSE for details.

Ranger is a self-hosting cross-language compiler for writing portable algorithms, parsers, generators, and small tools once and compiling them to multiple target languages.

It includes a compact typed language with classes, inheritance, traits, lambdas, type inference, extension methods, custom operators, and host integration through system classes.

Ranger is best approached today as a compiler and language lab with practical multi-target output, not as a polished general-purpose language ecosystem.

Ranger language = permissive. Ranger Gallery technology = copyleft.

The compiler does not put a license on a program you write and compile. The license of the output follows the source. Compiled gallery programs stay AGPL. Using EVG or another gallery/ module is using that framework, not only the language. Alternative commercial licenses may be available for gallery/ components. Full texts: LICENSE-MIT, LICENSE-AGPL-3.0, gallery/LICENSE. The path rule, the output rule, and third-party exceptions are in LICENSING.md.

What Ranger Is Good At

  • Writing one algorithm or tool and emitting several target languages from the same source
  • Building parsers, analyzers, generators, and DSL-like tooling with a small runtime surface
  • Experimenting with language design, operator templates, and code generation strategies
  • Studying a self-hosting compiler that is actively used to compile itself

Word of Warning

  • Ranger is still experimental, which means: be ready to fix bugs or add new capabilities when needed
  • Target quality varies by language and by feature area
  • The compiler is self-hosting, but the official and best-supported host is Node.js
  • Not every example in this repository works fully out of the box on every machine
  • Several examples in gallery/ are research or showcase projects and may require extra toolchains, platform-specific commands, or manual setup

If you want one sentence of positioning: Ranger is currently more convincing as a portable algorithm compiler / DSL toolchain than as a drop-in replacement for mainstream application languages.

@process runtime (experimental)

Ranger can mark classes with @process to get a small object runtime in generated code (especially JavaScript/TypeScript): parent/child tree, instance registry, proc_start / proc_stop, and UI refresh via markStateDirty().

| Piece | What it does | | --- | --- | | @process / @process(true) | Process instance with lifecycle hooks (start, stop, …) | | proc_start / proc_stop | Activate or tear down a process subtree (children first) | | proc_send target handler arg… | Typed message dispatch to fn on… handlers on a live instance | | ProcessNameRegistry.findProcess(path) | Lookup by @name("app.foo") (TypeScript path literals when using -typescript) | | markStateDirty() | Bump generation + notify host (ProcessUiHost) for UI binding | | beginSuppressUiNotify / endSuppressUiNotify | Batch parent↔child sync without notify storms |

Typical app shape: domain logic and UI flags live in @process classes; view DTOs are plain Ranger classes filled by a builder; React/CLI/native hosts subscribe to ProcessUiHost and call findProcess — I/O and async work stay in the host (Node, Swift, …), not inside the process bytecode.

class CounterPage @process @name("app.counter") extends RangerProcessBase {
  def count:int 0
  fn onUiIncrement:void () {
    count = count + 1
    this.markStateDirty()
  }
}
// Host (TypeScript): wire notify once, then bind UI to process fields
const host = ProcessUiHost.__singleton();
host.notifyPath = (path) => { /* sync view model + re-render */ };

Docs: PROCESS_MVP.md (scope), PROCESS_STATUS.md (compiler checklist), PROCESS_RUNTIME_INVARIANTS.md (dispatch turn / one notify), PROCESS_UI_NOTIFY.md (notify batching), PROCESS_UI_VIEW_MODELS.md (view DTO assignment). Gallery: process_counter_board (Vite + React host for @process).

Getting started with Claude Code

Two commands give a Claude Code session everything it needs to write Ranger — in any directory, including an empty one:

/plugin marketplace add terotests/Ranger
/plugin install ranger@ranger

Then, in a folder with nothing in it:

npm init -y && npm i -D ranger-compiler

…and ask for a program. The plugin carries the language's traps, a build command that fails when the compile fails (compilers through 3.5.1 printed [FAIL] and exited 0, which made && run the previous build), the EVG and Rave skills, and /ranger:example — worked examples indexed by what they do, with the command for each.

cp "$CLAUDE_PLUGIN_ROOT/examples/calc/Calc.rgr" .
rgr run Calc.rgr              # a parser and evaluator
rgr run Calc.rgr -l=python    # …the same source, through Python
rgr run Calc.rgr -l=go        # …and Go

plugins/ranger/README.md lists what is in it, and RangerStarter is the same thing as a project you clone: the skills are already in .claude/skills/, and npm start prints on a cold checkout.

Where To Start

  • Documentation site — install, first program, types, optionals, and the generated operator reference (838 operators, compiled from the sources of the commit that publishes the site, so it cannot drift)
  • The front page — what Ranger is, the targets, the platforms and the gallery (landing/)
  • Online playground — try Ranger in the browser (playground/, Vite + current compiler)
  • UI gallery — EVG controllers measured against Radix (controllers in gallery/ui/; the demos live in terotests/EVGUI)
  • README.md - language overview, installation, and syntax notes
  • gallery/README.md - index of the application stack (AGPL): EVG, Office, DataGrid, parsers, games, and @process host apps
  • LICENSING.md - MIT compiler vs AGPL gallery
  • TARGET_NOTES.md - what each target language supports and where it falls short
  • PLAN_FORMATS.md — the architecture for reading more than three file formats: the layer stack, the internal models, and the phased roadmap after DOCX/XLSX/PPTX. Phase 1 is .odp beside .pptx, run as the experiment that proves or disproves the shared scene
  • PLAN_API_DOCS.md — the design for doc { … } declarations: API metadata attached to a Ranger declaration that never restates what the compiler already knows, the no doc / doc / doc public visibility rule, a canonical ApiIR that names a logical module and no namespace, and the outputs built from it — native doc comments and annotations per target (XML doc, TSDoc, DocC, KDoc, rustdoc, Javadoc, dartdoc, Doxygen, docstrings), and the language × platform split that separates C# from Unity and Dart from Flutter
  • docs/plans/ — the plans that are still open: language, shapes, ownership, LLVM/WASM, the portable JS stdlib, formats and tooling. Finished ones move to legacy/docs/
  • CHANGELOG.md - version history
  • AGENTS.md — git/PR rules and Ranger gotchas for AI agents; links the FAQ
  • ai/ — short offline notes for assistants (README.md, QUICKREF.md, GRAMMAR.md, INTROSPECTION.md); prefer the docs site when online

Targets and compatibility

The compiler is self hosting: it is written in Ranger and compiled by itself, so it can run on several platforms. Node.js is the official host, because external plugins are only available as npm packages.

Primary targets — exercised by the test suite and large gallery programs: JavaScript / TypeScript (ES2015), Go, Python (3.x), Kotlin, C# (Mono mcs in CI; modern .NET also fine), Rust, Dart, Swift (3 and 6), and C++ (C++14).

The TypeScript/ES5 interpreter in gallery/game_engine/v2/interp is the largest cross-target gate: Go, Kotlin, Python, C#, Dart and Swift 6 each compile, and where the toolchain is on PATH they build and answer the Node benchmark cases (npm run test:tsengine). Dart also keeps the gallery/ts_parser golden (AST identical to the JS -d demo). Details: TS_ENGINE_PERF.md, TARGET_NOTES.md.

Thinner CI — PHP, Java 7 and Scala still have operator templates and appear in the syntax-app matrix, but they are not on the large-engine golden path. Treat them as usable with more gaps.

Older language versions can be supported by writing custom operators that target a compiler flag. Support is uneven:

| Area | Current expectation | | --- | --- | | Host/runtime | Node.js is the primary supported host for the compiler | | Self-hosting | Actively used. JavaScript is the reference; the compiler also compiles itself for C++, Dart, Python, C#, Go and Kotlin — each of those builds accepts the result and compiles the compiler again, all but C++ byte-for-byte identically (TARGET_NOTES.md) | | JavaScript / ES6 | Best baseline target and most reliable place to start | | Go / Python / Kotlin / C# / Rust / C++ | Large programs verified (TS engine and/or jpeg / parsers); expect remaining edge cases | | Dart / Swift | Strong for substantial modules (ts_parser; TS engine builds and matches Node on both when toolchains are present); Flutter packages via -l=dart -pubspec | | PHP / Java / Scala | Templates + syntax-app coverage; fewer end-to-end goldens | | LLVM / WASM (-l=llvm) | Experimental. Lowers to LLVM IR; optional WAT export for freestanding WASM. Native libc builds work for demos like Space Invaders; browser WASM is still rough. Another codegen path from the same sources, not a separate surface language. The compiler self-hosts on LLVM. It compiles for LLVM with no errors, the 22 MB of IR it emits for itself passes opt -passes=verify and links into a native rangerc — and that binary compiles the compiler, producing JavaScript byte-identical to the Node build's, which then reproduces itself (npm run selfhost:round:llvm). See TARGET_NOTES.md, npm run test:llvm, npm run selfhost:check:llvm, npm run game:build:llvm. | | Gallery examples | Good for understanding direction and capability, but some require manual setup or platform-specific tooling |

Targets are not equally demanding of the source. A target with a garbage collector — JavaScript, Go, Python, Kotlin, Dart, Java, C# — takes almost any Ranger source as written. A target without one — C++, Rust, and Swift's reference counting — needs the ownership annotations (@(weak), @(strong), @(lives), @(temp), see Annotations) wherever the object graph has a cycle or a non-owning reference. Code that runs on ES6 can therefore still fail to build, or leak, on C++ or Rust until those annotations are added. Write for the strictest target you intend to support, not for JavaScript.

Target languages documents what each target writes and the semantic differences a portable program has to know.

Conformance suite (Track 1)

Cross-target semantic fixtures live in tests/conformance/. Run npx vitest run tests/compiler-conformance.test.ts. Regenerate the fixture list with node scripts/generate-conformance-table.mjs.

| Fixture | Topic | Targets | | --- | --- | --- | | array_param_mutate | array parameters use reference semantics (Issue #58; Go known gap) | ES6, Go, Kotlin, Dart (when toolchain present) | | clear_then_push | clear resets slice without nil, push refills (Issue #59) | ES6, Go, Kotlin, Dart (when toolchain present) | | int_division_to_double | Conformance: integer division promoted to double (Issue #4) | ES6, Go, Kotlin, Dart (when toolchain present) | | lf_line_endings | LF-only source (Issue #12 class must not break operator spacing) | ES6, Go, Kotlin, Dart (when toolchain present) | | math_ops | Conformance: arithmetic and comparisons | ES6, Go, Kotlin, Dart (when toolchain present) | | string_codepoint_index | Conformance: Unicode code-point string indexing (Issue #57) | ES6, Go, Kotlin, Dart (when toolchain present) | | while_loop | Conformance: while loop control flow | ES6, Go, Kotlin, Dart (when toolchain present) |

Quick start

You do not need this repository to write a program in Ranger. This is the compiler, the gallery and their own test corpus — what you clone to work on Ranger. To start a program in it, RangerStarter is a project already built around the npm compiler: two source files, a test, a build loop that fails when the compile fails, all fourteen targets, and the agent skills. MIT.

git clone https://github.com/terotests/RangerStarter my-app
cd my-app && npm install
npm start          # hei maailma
npm run targets    # the same source as Swift, Kotlin, Rust, Go, C++, …

Source files use the .rgr extension (.clj is the legacy extension and still works) and the CLI is rgrc:

# Install globally
npm install -g ranger-compiler

# Compile to JavaScript
rgrc -l=es6 myfile.rgr -o=output.js

# Compile to TypeScript
rgrc -l=es6 -typescript myfile.rgr -o=output.ts

# Compile to Python
rgrc -l=python myfile.rgr -o=output.py

See CHANGELOG.md for the version history.


Common pitfalls

The things people hit first, in the order they usually hit them.

Ranger is still a Lisp. It reads like a braces-and-newlines language, but that surface is a set of shortcuts for S-expressions — the expressions are still there underneath. When something does not parse or does not type-check, the fix is almost always a pair of parentheses.

Negation needs the parentheses. ! is an operator like any other, so it takes its argument in prefix form:

if (! this.flag) { }     ; correct
if !this.flag { }        ; FAIL: Could not match argument types for if

Because the bare form does not compile, people fall back to if (this.flag == false). That works, but it is not needed — (! this.flag) is the direct form.

Singletons are an annotation, not a pattern you write by hand. Mark the class and call the generated accessor:

class Config @singleton(true) {
    def path:string "/etc/app"
}

def c (Config.__singleton())

Optionals have to be wrapped and unwrapped. Any variable declared without a value is optional, and several operators — get on a hash above all — always return one. Reading the value takes unwrap / !!, or ?? for a default; wrap makes an optional out of a plain value. Inside if (!null? x) { … } an optional object is narrowed: x.field and x.method() need no unwrap there. Forgetting this shows up as a type error between T and <optional>T. See Optional variables.

Extending the language does not mean recompiling the compiler. compiler/Lang.rgr is read at compile time, not baked into dist/rgrc.js. Add or change an operator template there, and the very next compile uses it — no npm run compile in between. The compiler looks for Lang.rgr in the working directory first, so a copy beside your sources overrides the installed one, which makes an experiment cheap to try and cheap to throw away.

Gallery and demos

The gallery/ folder holds the larger examples: parsers, EVG/TSX document tooling, games, and @process host apps. They show what the compiler can do, but they are research and showcase projects — some need extra toolchains or platform-specific setup. gallery/README.md indexes them and holds the writeups (build commands, benchmarks, target comparisons); each project also has its own README.

One of them is worth pointing at from here, because it is the clearest thing this compiler does. gallery/pptx reads .pptx decks and paints them through an EVG display list; gallery/pptx/android is the same source compiled to Kotlin and drawn with android.graphics — a real Android app with no PowerPoint code in it, because the ZIP reader, the OOXML parser, the theme resolver, the JPEG and PNG decoders, the TrueType reader and the layout engine are all Ranger and none of them were forked. The port is a facade, one walk over the display list, and two implementations of an eight-method surface — the second being Java2D, so a deck renders to PNGs on any JVM and the port can be checked without a device.

Calling other programs, and building an iOS app with no Xcode project

Ranger can run another command line program:

Import "lib/Shell.rgr"

def sh:Shell (new Shell)
def argv:[string]
push argv "rev-parse"
push argv "HEAD"
def r:ShellResult (sh.capture("git" argv))
if (r.ok()) {
    print (r.outText())
}

One compiler primitive is behind that — run_process_result, which answers ([exit code, stdout, stderr]) and can either capture the child's output or let it stream to this program's own. It takes a working directory and extra environment entries, passes the arguments as a vector so nothing inside one is ever re-read by a shell, and has a backend on thirteen targets. lib/Shell.rgr is the API over it: a result object, a log of every command line, and a dry run that records instead of executing.

That dry run is what makes a build driver testable, and lib/apple/ is the driver it was built for: AppleTarget, AppleAppSpec, AppleToolchain and AppleAppBuilder turn a list of Swift files into an installed, running iOS, iPadOS or watchOS app using xcrun, swiftc, plutil, codesign and simctl — no .xcodeproj, no xcodebuild, and nothing that opens Xcode. 151 checks assert the plan it produces (which program, which arguments, in which order) and they run on JavaScript, Python, Go, Rust, C++, Java and PHP, on any machine.

gallery/ui/ios is the whole of it worked through: the gallery/ui dashboard demo compiled to Swift, painted with CoreGraphics, and built for an iPhone, an iPad and an Apple Watch by a Ranger program.

npm run shell:test      # the process operator and lib/Shell, on this machine
npm run apple:test      # the Apple build driver, without a Mac
npm run ui:ios:plan     # the entire iOS build, printed and not run
npm run ui:ios:verify   # the port's own logic — 82 checks, no Mac
npm run ui:ios:run      # ...and on a Mac, the app on a simulator
npm run ui:ios:device   # ...or on the iPhone or iPad on the cable

ui:ios:device is one command because the three things a device build needs — the connected device, a codesigning identity and a matching provisioning profile — are all already on the machine, so they are found rather than typed.

Target-specific notes

Swift 6, Rust, the C++ static analysis optimizer, HTTP servers on the Go target, and the operator polyfill system are documented in TARGET_NOTES.md.

Testing

npm test                  # Run all tests
npm run test:es6          # JavaScript/ES6 tests only
npm run test:python       # Python target tests
npm run test:dart         # Dart target tests (requires Dart SDK)
npm run test:go           # Go target tests
npm run test:rust         # Rust target tests
npm run test:syntaxapp    # The syntax app on every target

ES6/JavaScript has full runtime coverage; Python and Go have compilation and runtime tests; Rust has compilation tests, with runtime tests in progress.

Syntax app (every target, measured)

tests/syntax_app/ is one program that uses 203 of the 207 core operator names of compiler/Lang.rgr together with classes, inheritance, traits, records, enums, extensions, lambdas, optionals, buffers, custom operators and the collection methods of lib/stdlib.rgr. npm run test:syntaxapp compiles it — and each of its sections on its own — to all fifteen targets the CLI accepts, then builds and runs the output with node, tsc, go, python3, rustc, g++, javac, php and lli, whichever of them the machine has, and compares what each one printed with the output of the reference target.

The result is a matrix that the test asserts against a checked-in baseline, so a target getting worse and a target getting better both fail until the record is updated (npm run test:syntaxapp:update). The current measurement is in tests/syntax_app/TARGET_REPORT.md and what does not work at all is in tests/syntax_app/known_gaps.md.

The JavaScript engine, built and measured

ComponentEngine is a JavaScript interpreter written in Ranger — so it compiles to every target the compiler has. Its sources are in terotests/componentengine (npm run deps puts them at gallery/componentengine); the builds, benchmarks and conformance suites below live in gallery/game_engine/v2/interp. Four commands build it and measure it, and each skips any target whose toolchain is not installed.

npm run jsengine:check         # report which C++/libstdc++ toolchain is usable
npm run jsengine:build         # es6 module + cpp and rust binaries
npm run jsengine:bench         # against Node, and QuickJS if `qjs` is on PATH
npm run jsengine:conformance   # 1303 JS probes, answers checked against Node
npm run jsengine:all           # all three in order

jsengine:build needs a C++ compiler for the cpp target and rustc for the Rust one; without them it still writes the generated source and says which it skipped. Run npm run jsengine:check first if a native build fails — it probes for a compiler that can use -cpp-single-thread (libstdc++ __shared_ptr / _S_single).

On macOS, Apple's Command Line Tools g++ is clang + libc++ and cannot build that fast path. Install real GCC:

brew install gcc          # provides g++-14 / g++-15 / …
npm run jsengine:check    # should report rg_ptr: OK
npm run jsengine:build

Without Homebrew GCC the build still proceeds, but omits -cpp-single-thread (atomic std::shared_ptr — correct, slower). Set CXX to pin a compiler. jsengine:bench reports one table across every engine that got built:

case           node       qjs       es6       cpp      rust   vs node   ok
loop          0.031     0.530     2.574     1.574     2.229    51.4x   OK
fib           0.134     0.690     7.079     6.380     6.262    46.9x   OK
...
against QuickJS (lower is better, 1.0x would be parity):
  es6    4.0x   (6.2x excluding strcat)
  cpp    2.9x   (4.7x excluding strcat)
  rust   3.1x   (5.0x excluding strcat)

Two things about that table are worth knowing before quoting it. Every row is checked against Node's answer and the command exits non-zero if any engine disagrees — a fast wrong answer is not a result. And the strcat row flatters us: QuickJS 2021-03-27 has no string ropes, so its s += "ab" is quadratic. Quote the excluding-strcat number. QUICKJS_COMPARISON.md has the measurement methodology and a source-level comparison of what the two engines do differently.

jsengine:octane runs the same Octane v9 suites published on zoo.js.org — Richards, DeltaBlue, Crypto, RayTrace, EarleyBoyer, RegExp, Splay and NavierStokes — on every target that was built, and places the result against same-machine Node and the published amd64 V8 column.

Octane’s harness times suites with performance.now (fractional liveClock ms). Prefer that over quoting older tables that used new Date() — Date is TimeClip’d to whole milliseconds and pinned many native scores to a few discrete buckets. jsengine:bench still times from outside the engine and is the one to use for cross-target microbenchmarks.

For the engine's own test coverage:

npm run jsengine:test          # the runtime-conformance suite, in-process
npm run jsengine:test:targets  # the engine compiled to every other target

Known issues

toString as a method name crashes the compiler (use getSymbol or a similar name), the Go target has integer division type conversion issues, and the Python target has inheritance constructor argument issues. See ISSUES.md for the full list and status.

AI documentation

Language questions for humans and agents belong on the documentation site, especially the FAQ. Repo-local agent notes:

  • AGENTS.md — git/PR workflow and syntax gotchas
  • ai/README.md — index of what remains under ai/
  • ai/QUICKREF.md — offline syntax card
  • ai/GRAMMAR.md — simplified BNF and operator-template notation
  • ai/INTROSPECTION.md — compiler introspection API (type at line/column, class shape) for IDE hover, autocomplete, and type-safe generation

The long ai/INSTRUCTIONS.md / ai/EXAMPLES.md guides were removed; they duplicated the docs site and had drifted (.clj paths, obsolete operators).

Installing the compiler

Install the compiler from npm:

 npm install -g ranger-compiler

Running ranger-compiler without arguments shows available command-line options:

Ranger Compiler v3.0.1

Usage: rgrc <file> [options] [flags]
Options: -<option>=<value>
    -l=<value>             Selected language, one of es6, go, scala, java7, swift3, swift6, kotlin, cpp, php, csharp, python, rust
  -d=<value>             output directory, default directory is "bin/"
  -o=<value>             output file, default is "output.<language>"
  -classdoc=<value>      write class documentation .md file
  -operatordoc=<value>   write operator documention into .md file
  -apidoc=<value>        write the API documentation artifacts into this subdirectory
  -apiformat=<value>     which API artifacts to write: json, markdown, report (default json,markdown)
  -csnamespace=<value>   C# namespace for the generated types
  -ktpackage=<value>     Kotlin package for the generated types
Flags: -<flag>
  -apipackage    Write the packaging the target ecosystem expects (package.json for npm, .csproj and docfx.json for NuGet)
  -apistrict     An undocumented public declaration or parameter is an error, not a warning
  -keep-examples Emit the functions named by `example`. They are type checked either way; by default they are left out
  -forever       Leave the main program into eternal loop (Go, Swift)
  -allowti       Allow type inference at target lang (creates slightly smaller code)
  -plugins-only  ignore built-in language output and use only plugins
  -plugins       (node compiler only) run specified npm plugins -plugins="plugin1,plugin2"
  -strict        Strict mode. Do not allow automatic unwrapping of optionals outside of try blocks.
  -typescript    Writes JavaScript code with TypeScript annotations
  -npm           Write the package.json to the output directory
  -nodecli       Insert node.js command line header #!/usr/bin/env node to the beginning of the JavaScript file
  -nodemodule    Export classes as CommonJS modules using module.exports (disables static main function)
  -esm           Export classes as ES6/ESM modules using export keyword (disables static main function)
  -sourcemap     Emit .js.map / .ts.map with embedded .rgr sourcesContent (ES6/TypeScript only)
  -client        the code is ment to be run in the client environment
  -scalafiddle   scalafiddle.io compatible output
  -compiler      recompile the compiler
  -copysrc       copy all the source codes into the target directory
Pragmas: (inside the source code files)
   @noinfix(true)   disable operator infix parsing and automatic type definition checking

JavaScript Module Formats

The compiler supports three JavaScript module output formats:

| Flag | Format | Output | Use Case | | ------------- | -------- | ------------------------- | -------------------------------- | | (none) | Plain JS | No exports, runs main() | Standalone scripts | | -nodemodule | CommonJS | module.exports.X = X; | Node.js require() | | -esm | ES6/ESM | export class X | Modern ES modules, import/export |

Examples:

# Standalone JavaScript (runs main function)
node dist/rgrc.js -es6 myfile.rgr -o=myfile.js

# CommonJS module (.cjs)
node dist/rgrc.js -es6 -nodemodule myfile.rgr -o=myfile.cjs

# ES6/ESM module (.mjs)
node dist/rgrc.js -es6 -esm myfile.rgr -o=myfile.mjs

File Extensions: The compiler automatically detects JavaScript-related extensions (.js, .ts, .mjs, .cjs) and won't double-add them. You can safely specify the full filename with extension.

JavaScript / TypeScript source maps (-sourcemap)

Use -sourcemap with -es6 or -typescript to emit a sibling .js.map / .ts.map file and append //# sourceMappingURL=… to the generated output. Kotlin, Swift, and other non-JS targets ignore this flag.

What you get

| Output | Purpose | | --- | --- | | output.js.map | Source map v3 (VLQ mappings, names, sources) | | sourcesContent | Full .rgr source embedded in the map — Chrome DevTools can open .rgr files without a separate file server | | Statement + expression mappings | LiveCompiler.WalkNode walk context plus outMapped() on identifiers, calls, literals |

Compile example

# Standalone ES module + map
node dist/rgrc.js -es6 -esm -nodemodule -sourcemap ./myapp/App.rgr -o=app.js

# Result: bin/app.js and bin/app.js.map

Debug in Chrome / Edge

  1. Serve the generated .js (and .map beside it). Vite/webpack are optional when sourcesContent is embedded.
  2. Open DevTools → Sources. Original .rgr files appear under the map tree (from sourcesContent).
  3. Set breakpoints on executable lines (e.g. def, if, return) — not only blank lines or signatures.
  4. Breakpoints must be solid red. A hollow/grey breakpoint means no mapping for that line; rebuild with -sourcemap and hard-refresh (disable cache).

Tests

npm run compile
npx vitest run tests/compiler-sourcemap.test.ts

Implementation notes (for compiler hackers)

  • compiler/SourceMap.rgr — SourceMapBuilder, VLQ encoder, addMappingFromNode() uses node.getLine() + node.code.getColumn(sp) (not stale node.row).
  • compiler/CodeWriter.rgr — lineNumber / columnNumber on emit, walkNodeStack, outMapped(), .map write in CodeFileSystem.saveTo.
  • Flag: compiler/Compiler.rgr → flag sourcemap; enabled in VirtualCompiler.rgr via fileSystem.enableSourceMaps().

Packages: ranger.json and rgrc install

A Ranger project names the packages it needs in a ranger.json next to its sources. rgrc install fetches them, records exactly what it fetched in ranger.lock, and the compiler resolves Import "pkg:…" against both. There is no registry: a package is a directory with a ranger.json, taken from a path on disk or from a public Git repository over HTTPS.

{
  "name": "myapp",
  "version": "0.1.0",
  "entry": "src/Main.rgr",
  "license": "MIT",
  "dependencies": {
    "evg": {
      "git": "https://github.com/terotests/evg.git",
      "rev": "f3d682ad11d1944251b683e5013ee624be421259",
      "subdir": "storm"
    },
    "componentengine": {
      "git": "https://github.com/terotests/componentengine.git",
      "rev": "32435100570ed4b8d08d94f8392c4c9f63bc5e45",
      "subdir": "engine"
    },
    "util": { "path": "../util" }
  }
}

| Field | | | --- | --- | | name | the package's name; pkg:<name> inside the package itself means the package | | entry | the file Import "pkg:<name>" (no path) resolves to | | version, license, description | recorded, not interpreted (no semver solving) | | dependencies | name → where it comes from, one of the forms below |

| Dependency | | | --- | --- | | { "path": "../util" } | a directory on disk, relative to this ranger.json | | { "git": "<https url>", "rev": "<commit or branch>" } | a public Git repository at a revision; a branch is pinned in the lock to the commit it pointed at | | { "git": "<https url>", "tag": "<tag>" } | the same, by tag | | "subdir": "<dir>" | with git: the package is that directory of the repository, and only it is fetched |

rgrc install                 # the nearest ranger.json, from the working directory up
rgrc install -frozen         # CI: fail rather than fetch what ranger.lock does not cover
rgrc install -force          # fetch again even when the locked commit is in the cache
rgrc install -vendor         # also write vendor/ranger/<name>, so no cache is needed
rgrc install -cache=<dir>    # instead of RANGER_PKG_CACHE, else ~/.cache/ranger/packages

Then, in the sources:

Import "pkg:evg/EVGElement.rgr"        ; a file of the package
Import "pkg:componentengine"           ; the package's entry
Import "./Local.rgr"                   ; this package

What install does. A git dependency is not a clone: one smart-HTTP round fetches the commit and its trees, a second only the files under subdir. The checkout goes into the cache under the SHA-256 of its content, and ranger.lock records the commit and that hash. Commit ranger.lock; a clean checkout then compiles the same sources, and -frozen makes CI prove it. A dependency the lock pins and the cache holds is not fetched again; changing git or rev in ranger.json overrides the lock.

Dependencies of dependencies. Install reads each fetched package's own ranger.json and fetches what it names, breadth first. A path dependency inside a fetched package means a sibling in the same repository at the same commit (evg's "image": { "path": "../image" } fetches image/ from terotests/evg). The first entry for a name wins, and the project's own ranger.json is read first — so a project that names a package itself decides which copy every package in the build gets. That is how one build avoids two copies of the same classes: if the project uses evg and a dependency asks for evg at another commit, the project's entry is the one.

How pkg: resolves. From the file that has the Import, the compiler walks up to the nearest ranger.json; if that manifest cannot place the package, the manifests of the files that imported this one are tried, ending at the project the compile started from. In each: a path dependency that is on disk, else vendor/ranger/<name>, else the cache entry ranger.lock names. ../x/A.rgr and pkg:x/A.rgr that land on the same file are one import.

Protocol details, the cache layout and the tests are in pkg/README.md.

Packages this repository takes from elsewhere

EVG, ComponentEngine and Vela have repositories of their own; this tree names them in the root ranger.json and does not track their files:

| Package | Repository | Placed at | | --- | --- | --- | | evg (EVG layout engine), image (codecs) | terotests/evg storm/, image/ | lib/evg, lib/image | | componentengine (the JavaScript/TSX evaluator) | terotests/componentengine engine/ | gallery/componentengine | | cer (the evaluator as a strict Rust module) | terotests/componentengine cer/ | gallery/cer | | vela (the Vega-compatible chart runtime) | terotests/VelaCharts vela/ | gallery/vela |

npm ci (through prepare) and npm run deps run rgrc install against the root ranger.json and copy each package to its place (scripts/deps.mjs), so every path the gallery uses — "evg": { "path": "../../lib/evg" }, "componentengine": { "path": "../componentengine" }, the Import "../../vela/src/…" lines, the web builds that copy lib/evg/gl/*.js — finds them. Those directories are git-ignored; do not edit them here.

To change one: commit to its repository, put the new commit in the root ranger.json ("rev"), run npm run deps, and commit ranger.json and ranger.lock. npm run deps -- --from=../evg puts a local checkout's working tree in place instead, to try a change against the gallery before committing it; npm run deps -- --check fails when the placed copies do not match the lock.

Getting started with Hello World

Create file hello.rgr

class Hello {
    sfn main () {
        print "Hello World"
    }
}
ranger-compiler hello.rgr            ; writes bin/hello.js
ranger-compiler hello.rgr -o=hello.js

The first program walks through the same program and shows the output the compiler writes for Go, Python and Rust.

Compiling using TypeScript

The compiler can be used from TypeScript, which makes possible to create new versions of the compiler just using TypeScript.

Note: the example requires Lang, stdlib, stdops, and JSON to be loaded for the compiler. In this example they are loaded from the filesystem using readFileSync.

// Notice this part of example is required:
addFile("Lang.rgr", fs.readFileSync("./libs/Lang.rgr", "utf8"));
addFile("stdlib.rgr", fs.readFileSync("./libs/stdlib.rgr", "utf8"));
addFile("stdops.clj", fs.readFileSync("./libs/stdops.clj", "utf8"));
addFile("JSON.clj", fs.readFileSync("./libs/JSON.clj", "utf8"));

The full compiler code:

import * as R from "ranger-compiler";
import { CodeNode } from "ranger-compiler";

const compilerInput = new R.InputEnv();
compilerInput.use_real = false;

// manually create a filesystem
const folder = new R.InputFSFolder();
const addFile = (name: string, contents: string) => {
  const newFile = new R.InputFSFile();
  newFile.name = name;
  newFile.data = contents;
  folder.files.push(newFile);
};
addFile(
  "hello.clj",
  ` 
class hello {
    static fn main() {
        print "Hello World"
    }
}  
`
);

// compiler requires language definition and libraries to work
const fs = require("fs");
addFile("Lang.clj", fs.readFileSync("./libs/Lang.clj", "utf8"));
addFile("stdlib.clj", fs.readFileSync("./libs/stdlib.clj", "utf8"));
addFile("stdops.clj", fs.readFileSync("./libs/stdops.clj", "utf8"));
addFile("JSON.clj", fs.readFileSync("./libs/JSON.clj", "utf8"));

compilerInput.filesystem = folder;

// set compiler options -l=es6 -typescript
const params = new R.CmdParams();
// target language is Go
params.params["l"] = "go";
params.params["o"] = "hello.go";
params.values.push("hello.clj");
compilerInput.commandLine = params;

// Run compiler
const vComp = new R.VirtualCompiler();

// Check results...
const res = await vComp.run(compilerInput);

// browse through the target compiler file system
res.fileSystem.files.forEach((file) => {
  console.log(file.getCode());
});

Switching to different target language

-l=<language> selects the target; running the compiler with no arguments lists the available ones, and the supported versions are under Targets and compatibility. JavaScript additionally has -typescript, which adds TypeScript annotations to the generated source.

Target languages documents what each target writes for the main function, and the semantic differences a portable program has to know (integer division, the sign of %, string indexing, reference counting).

Operators

Operators are short, typed commands like get or push. The compiler holds no code for them, only one emission template per target language, which is how platform-specific code and native polyfills are expressed. M_PI in compiler/Lang.rgr is a small example:

    M_PI mathPi:double () {
        templates {
            es6 ("Math.PI")
            go ( "math.Pi" (imp "math"))
            swift3 ( "Double.pi" (imp "Foundation"))
            java7 ( "Math.PI" (imp "java.lang.Math"))
            php ("pi()")
            cpp ("M_PI" (imp "<math.h>"))
            csharp ("Math.PI" (imp "System"))
        }
    }

A target with no template of its own, and no * fallback, emits nothing — so adding a target to an operator is a one-line change. Operators can also be written as macros in Ranger itself.

Reference: the operator concept page explains templates and the second mechanism (type methods, which every target gets for free), and the generated reference lists every operator with its signature, per-target support, and compiled example output. The -operatordoc=<file> compiler option writes the same kind of table locally when you need one without a network.

Plugins

Compiling as CommonJS module:

ranger-compiler hello.clj -npm -nodemodule

Compiling as ES6/ESM module:

ranger-compiler hello.clj -npm -esm

Example

Import "VirtualCompiler.clj"

flag npm (
  name "hello"
  version "0.0.1"
  description "Plugin Hello World"
  author "Tero Tolonen"
  license "MIT"
)

class Plugin {
  fn features:[string] () {
      return ([]  "postprocess")
  }
  fn postprocess (root:CodeNode ctx:RangerAppWriterContext wr:CodeWriter) {
    print "*** plugin postprocess was called ***"
  }
}

Notes about the syntax

The rest of this README is the language reference. The documentation site covers part of the same ground in shorter, edited pages — program structure, types and optional values — and those pages carry the per-target detail (what an optional compiles to in Go, Rust and Swift, for instance) that the sections below do not. What follows here is the material the site does not have yet: traits, custom operators, system classes, unions, class extensions, and the annotation list.

Ranger syntax is originally based on Lisp -language syntax and most operators will use prefix notation. However, the Ranger modifies the original Lisp so that inside block expression { ... } there is no need to insert parenthesis which makes the language appear to be a bit more like standard languages. Thus you can write exressions like

class Hello {
    fn sayHello:void () {
        def x 20
        if ( x < 10 ) {
            print "x < 10"
        } {
            print "x >= 10"
        }
    }
}

However, when you go deeper in the expression you may have to include the parenthesis, for example when invoking object you have to write

def obj (new Hello)

For most common mathematical symbols and boolean operators infix notation can be used and they are automatically converted to lisp expressions. Thus you can write expressions such as (x + y * z) instead of (+ x (* y z))

def x 100
def y 200
def z ( x + y * 10)
if ( x < 20 || y == 0 ) {

}

The assigment operator is also automatically prefixed from infix notation so you can say

x = y

Instead of common lisp syntax (= x y)

Functions, main and comments

fn declares a function of an object and sfn a static function of the class. Each file can have a static main function, which is the entry point of the program. A function that is not void must return a value with return. A comment starts with ;.

; here is a comment
class Hello {
    sfn main () {
        def o (new Hello)
        o.SomeNonStaticFn()
    }
    fn SomeNonStaticFn () {
    }
    sfn SomeStaticFn () {
    }
}

Hello.SomeStaticFn()   ; calling a static function of a class

Program structure covers class, record, systemclass, Import, Extend and Enum in one table, and explains how blocks are passed to operators.

API documentation: the doc { } tail

A declaration can carry a doc { … } block as its tail, after the body. The signature stays clean and the documentation is visibly an attachment to the declaration rather than a part of the program. It is compiler metadata, not a comment: ; still documents the implementation, doc documents the interface.

class EVGA11yTree {
    def focusId:string "" doc {
        public
        description "The identifier of the currently focused node."
    }

    fn find:EVGA11yNode ( id:string ) {
        ...
    } doc {
        public
        description "Finds an accessibility node by its stable identifier."
        param id "The stable accessibility identifier."
        returns "The matching node."
        since "1.2"
        see EVGA11yNode
    }

    fn rebuildIndex:void () {
    } doc {
        description "Rebuilds the lookup index. Not part of the public API."
    }
} doc {
    public
    description "A platform independent accessibility tree."
}

The block never restates what the compiler already knows. There is no param x int and no returns void: the types come from the signature, and a param line that carries a type is a compile error. What the doc block carries is what no analysis can recover — prose, audience, version history and cross-references.

Documentation is the API declaration. There is no export fn keyword:

no doc block            -> internal, undocumented
doc { … }               -> documented, internal
doc { public … }        -> exported public API

An undocumented function has no spelling for public. The block is checked against the signature it documents, which is the point of it being structured: a param that names no parameter, a param documented twice, a returns on a void function and a public member of an internal class are all compile errors.

Entries: public, internal, description, param, returns, throws, since, deprecated { since use description }, see, example, category, experimental, platform, and target <name> { … } for markup that only one language has.

example names a function, not a string. The sample is compiled and type checked with the rest of the program, rendered into each target's doc comment in that target's syntax — const g = new Greeter() on JavaScript, g.greet(name : "world") on Swift — and then left out of the emitted code. -keep-examples puts it back.

fn greet:string ( name:string ) {
    ...
} doc {
    public
    description "Builds a greeting for a name."
    example greetExample
}

class GreeterExamples {
    sfn greetExample:void () {
        def g:Greeter (new Greeter())
        print (g.greet("world"))
    }
}

The compiler writes the documentation into the generated code in the target's own form and, with -apidoc=<dir>, a target-independent api.json, a Markdown reference and an api.txt report of the public surface. -apipackage adds the packaging the ecosystem expects:

# an npm package documentation.js renders with no configuration
rgrc -es6 a11y.rgr -d=out -o=index.js -nodemodule \
  -apidoc=docs -apipackage -name=evg-a11y -version=1.2.0 -license=MIT

# a NuGet project whose XML documentation DocFX reads
rgrc -l=csharp a11y.rgr -d=out -o=EvgA11y.cs \
  -apidoc=docs -apipackage -name=Evg.A11y -version=1.2.0 -license=MIT

# a pub package `dart doc` renders. The layout matters: dart doc reads lib/
# and skips lib/src/, so `public` decides what the documentation shows.
rgrc -l=dart a11y.rgr -d=pkg/lib/src -o=evg_a11y_impl.dart \
  -apidoc=docs -apipackage -name=evg_a11y -version=1.2.0

Six targets carry the documentation into the generated code today:

| Target | Comment form | Packaging | Doc tool | | --- | --- | --- | --- | | JavaScript | JSDoc (@param {string} id, @public / @private) | package.json | documentation.js | | C# | XML docs (<summary>, <param>, <seealso cref>, [System.Obsolete]) | .csproj, docfx.json | DocFX, Sandcastle | | Kotlin | KDoc (@param, @return, @see, @Deprecated(… ReplaceWith)) | build.gradle.kts | Dokka | | Swift | DocC (- Parameter, - Returns, > Since:, @available) | Package.swift, .docc catalog | DocC | | Python | Google docstrings (Args:, Returns:, .. versionadded::) | pyproject.toml, __all__ | pdoc, Sphinx | | Dart | dartdoc (/// [id] …, [Symbol], @Deprecated) | pubspec.yaml, generated export … show | dart doc |

Each also gets a namespace, package or export surface from the doc blocks. On Dart and Python public writes the export list itself — a barrel file and __all__ are exactly the kind of list that rots when a person maintains it. A class with no doc block is not opted into the API model and compiles exactly as it did before. Design and the remaining targets: PLAN_API_DOCS.md.

Types

Type inference determines the type of local variables and class properties, or the program declares it after a colon:

def x 100      ; inferred type = int
def y:int 200
def o (new myClass) ; inferred type myClass

The primitive types are int, boolean, string, double, char and charbuffer, plus the fixed-width integer types; a function that returns nothing is void. Arrays, hashes and anonymous functions are usable as variable types but need a signature. Enum, class, systemclass, systemunion and trait need a declaration of their own.

Types has the full table with example values, the buffer types and what each one compiles to.

Strings, enums and collections

String literals use JSON escaping rules and can be multiline:

def long_string "
    this is
    a multiline string
"

The rest moved to the documentation site, where each operator also shows the code it writes per target: Strings (the operator set, concatenation, the code-point index rule) and Types (arrays, hashes, get returning an optional, and Enum).

Anonymous functions / lambdas

Anonymous function type declaration is automatically inferred

def name "foo"
def myFilter (fn:boolean (param:string) {
    return (param == name)
})
if(myFilter("foo")) {
    print "it was foo"
}

To give declare Anonymous function as parameter of function you must include the full signature, for example for a callback taking string and int signature is fn:void (txt:string i:int)

fn foo:void ( callback:( fn:void (txt:string i:int)) ) {
    callback("got this?" 10)
}

When giving lambda as a parameter, the formal type definition can be omitted, the named parameters are automatically declared to the block scope of the lambda.

this.foo({
    print txt + " = " i
})

Lambdas compile on every backend, including the freestanding WASM/WAT path (-wasmrc): each body is hoisted to a function-table entry and calls go through call_indirect, with the value a reference-counted closure record. Captured variables are copied (values/strings) or retained (objects); a captured object can be mutated through the closure, and a captured value can be shared by boxing it in a heap cell.

Automatically infixed math support

It is easy to define new mathematical operations in the Lang.clj file or in modules. However, some mathematical operations are automatically infixed for easier usage. Thus, instead of using common lips notation (* 4 10) you can use easier to read infixed 4 * 10 -syntax

Boolean logic operators

a && b
a || b

Math operators

a * b
a / b
a - b
a + b

Logical comparisions

a < b
a <= b
a > b
a >= b
a != b

Common set of Operators and the Grammar file

The file compiler/Lang.rgr holds the common set of operators and the compilation rules. The most common operators — to_double, read_file, array_length and the rest — are defined there, and the generated reference is built from it. Editing the file makes it easy to extend the language with new operators or to change existing rules, but it describes the common set of rules and should be edited sparingly, not daily.

The file has couple of sections, but the reserved_words and commands. The Reserved words section declares (surprise!) the reserved words and their transformation. This is required because for example in Go the word map is a keyword and can not be used unless it is conveted to some other name, for example to FnMap.

    reserved_words {
        * {
            map FnMap
            forEach forEachItem
            self _self
            func _func
        }
        cpp {
            operator _operator
            static _static
            union _union
            bool _bool
            ref _ref
            class _class
            new _new
            delete _delete
            template _template
            namespace _namespace
            virtual _virtual
            public _public
            private _private
            protected _protected
        }
        go {
            type _type
        }
        rust {
            type r#type
            static r#static
            ref r#ref
            union r#union
            bool r#bool
        }
        swift3 {
            operator _operator
            static _static
            init _init
        }
        swift6 {
            operator _operator
            static _static
            init _init
        }
    }

The * section defines global mappings that apply to all target languages. Language-specific sections (like cpp, rust, go, swift3, swift6) define additional reserved word mappings for that particular target. For Rust, the r# prefix is used to escape keywords (raw identifiers).

What the result should be is of course highly opinionated. In this example, the line map FnMap means that if possible the compiler will transform anything named map to fnMap if possible. If transformation is not possible, compiler error is generated.

The common operators are declared in section commands, which describe commands, their expected parameters and return values and rules on how they should be compiled into the target languages, possible imported libraries and possible macros or helper function which should be created if the operator is used.

Example of simple operator is (M_PI) which will return double value of mathematical symbol "pi".

    commands {
        M_PI mathPi:double () {
            templates {
                es6 ("Math.PI")
                go ( "math.Pi" (imp "math"))
                swift3 ( "Double.pi" (imp "Foundation"))
                java7 ( "Math.PI" (imp "java.lang.Math"))
                php ("pi()")
                cpp ("M_PI" (imp "<math.h>"))
            }
        }
        ...

Most operators are simple, but some require creating custom macros, helpoer functions and some of them are so complex that they may be implemented in the compiler core.

Modules, classes and operators

The basic unit of the program is class. The functions of classes can not be overloaded at the moment, which means that you can not have two functions with different parameters or different return values.

Each source file can import other files using Import command.

Import "Vec2.clj"

class vectorTest {
    fn testVectors () {
        def v (new Vec2 ( 5 4 ))
    }
}

Class declaration

class fatherClass {
    def msg "Hello "
    fn foo:string ( txt:string ) {
        return (msg + txt)
    }
}
class childClass {
    Extends( fatherClass )
}
class mainProgram {
    sfn main () {
        ; invoke the class
        def cc (new childClass)
        cc.foo("World!")
    }
}

Class constructor

class myClass {
    def name:string ""
    Constructor (n:string) {
        name = s
    }
}

Notes:

  1. currently only a single variant of the constructor is possible.
  2. as of this writing calling the parent class constructor does not work properly

Class invocation

def obj (new myClass ("name"))

classes without constructor can be invocated without arguments

def obj (new simpleClass)

Creating a class extension

Class extensions are useful for keeping classes simple and moving dependencies to external Modules which can extend the classes.

Extension can

  • add new functions to the class
  • add new member variables to the class
extension childClass {
    def name:string ""
    fn bar:string ( txt:string ) {
        return ("Hello from exteision: " + txt)
    }
}

Optional variables

An optional value must be unwrapped before use, and unwrapping a non-nullable value is a compiler error. Any variable declared without a value is optional, which corresponds to the Swift ? type. The @(optional) annotation declares one explicitly, and some operators — (get <hash> <key>) among them — always return one.

    def item@(optional):myClass

    def strMap:[string:string]
    def str (get strMap "myKey")
    if(!null? str) {
        print (unwrap str)
    }

Inside the then block of if (!null? x) an optional object is narrowed: fields and methods are read through it without unwrap, also under -strict. A condition narrows only when it must be true for the block to run — a single !null? or an && of them, on a name or a path such as a.friend:

    fn describe:string (p@(optional):Person) {
        if (!null? p) {
            return (p.greet() + " " + p.name)
        }
        return "nobody"
    }

if p { … } is the same test. Inside the block def q:Person p takes the value: the compiler writes one unwrap for it, the same code as def q:Person (unwrap p).

The flow narrows as well: the else branch of if (null? p), the code after if (null? p) { return … } (also throw, break and continue), and the code after p = <a value>:

    fn describe:string (p@(optional):Person) {
        if (null? p) {
            return "nobody"
        }
        return p.name
    }

Not narrowed: an || of !null? tests, and optional scalars — (n + 1) on an optional int still needs (unwrap n).

A field declared without a value is optional too. Under -strict it counts as present when the constructor assigns it at its top level, and a field that an attach or init method sets before use is declared @(late) (def model@(late):SheetModel), like Kotlin's lateinit.

Optional values lists the operators (??, !!, unwrap, null?, !null?, wrap, nullify), what each target uses for an empty value, and the -strict flag.

Two warnings that apply to the current implementation. Optionals are not "safe" in the sense of preventing programming errors: a variable that was unwrapped automatically can still be misused. Making them safer is planned, and the options are being considered. Ranger also does not protect against mistakes when automatically unwrapping long reference chains such as obj.property.subProperty.foo, where property and subProperty are optional.

Control flow

if

If statement is quite similar to other language, but then and else keywords are not used

def x 100
if ( x < 10 ) {
    ; then branch
} {
    ; else branch
}

switch - case

Note: currently case statement does not support multiple matching values, it is planned to add support for that later.

def name "John"
switch name {
    case "John" {

    }
    case "Flat Eric" {

    }
    default {

    }
}

Loops

for -loop

def list:[string]
for list s:string i {
    print s
}

You can use break and continue to control the for -loop.

while -loop

def cnt 10
while (cnt > 0 ) {
    print "round " + cnt
}

You can use break and continue to control the while -loop.

Custom operators

One of the most important features or Ranger is the ability to create custom operators which can target some specific language or all languages using macros. Together with systemclass they allow the system to integrate to target environment or to create new abstraction over existing native API's.

Operators allow type matching against

  • defined primitive types
  • defined classes
  • Enums
  • optionality
  • traits

Operators can be writing directly target language construct or they can be macros, which write code in Ranger and the compiler will then transform the resulting AST tree into the target language's code using the conventions of target language. Which is better depends on the situation, for example operators for system classes usually are written directly to the traget language while operators which are using Ranger's own classes or datatypes are usually better to write with macros.

Simple example of useful macro is Matrix and Vector multiplication. Let's say that you have defined a Matrix class and want to overload the * -operator for easy matrix multiplication.

class Mat2 {
  def m0 1.0
  def m1 0.0
  def m2 0.0
  def m3 1.0
  def m4 0.0
  def m5 0.0
  fn multiply:Mat2 ( b:Mat2 ) {
      def t0 (m0*b.m0 + m1 * b.m2)
      def t2 (m2*b.m0 + m3 * b.m2)
      def t4 (m4*b.m0 + m5 * b.m2 + b.m4)

      def res (new Mat2)
      res.m1 = (m0 * b.m1 + m1 * b.m3)
      res.m3 = (m2 * b.m1 + m3 * b.m3)
      res.m5 = (m4 * b.m1 + m5 * b.m3 + b.m5)
      res.m0 = t0
      res.m2 = t2
      res.m4 = t4
      return res
  }
}
operators {
    *  base:Mat2 ( a:Mat2 b:Mat2) {
        templates {
            * @macro(true) ( (e 1 ) ".multiply(" (e 2) " )" )
        }
    }
}

The * @macro(true) means that we target all languages and this is a macro, not actual target language construct.

Custom operators and System classes

To integrate with the target languages running environment, Ranger modules can declare systemclass which can be used together with the code.

systemclass DOMElement {
    es6 DOMElement
}

operators {
    find  base:DOMElement ( id:string) {
        templates {
            es6 ("document.getElementById( " (e 1) " )")
        }
    }
    setAttribute  _:void ( elem:DOMElement name:string value:string) {
        templates {
            es6 ( (e 1) ".setAttribute(" (e 2) ", " (e 3) ")" )
        }
    }
}

class tester {
    fn modifyDom () {
        def e (find "#someelem")
        setAttribute( e "className", "activeElement")
    }
}

Note: Definition of system classes will be revisited in near future and there will be potentially small changes to it.

Unions of system classes

Sometimes the system class can be of union type. This means that the traget language can accept multiple types in place of a single type.

systemunion DOMElementUnion ( DOMElement string )

The you can create operator which accepts either DOMElement or string and reduces that to a single type.

Traits

Traits are like extensions, which can be plugged into several classes using does keyword.

Traits

trait bar {
    fn hello() {
        print "Hello"
    }
}

; foo implements "bar" trait
class foo {
    does bar
}

Traits are very useful when used together with custom operators, because operators can also match traits.

Another useful feature of traits is their genericity. Traits take type parameters, and so do classes — see Generic classes below — so a generic collection can be written either way.

trait GenericCollection @params(T S) {
    def items:[T]
    fn  add (item:T) {
        push items item
    }
    fn  map:S ( callback:( f:T (item:T))  ) {
        def res:S (new S ())
        for items ch@(lives):T i {
            def new_item@(lives):T (callback (ch))
            res.add(new_item)
        }
        return res
    }
    ; ... TODO: add more collection functions...
}

; then create a specific "s