Go to file T
Code
.github Gate native entropy and wasm compilation without another CI slot
assets assets: the wolf mark; readme carries it
ci Clarify the incidental shim-search match
docs Bump lupin to 0.1.56
examples docs: manual chapter 6, the place trace — schema version 1 and examples/pack.lu (doc-truth checked)
src Keep the site portability patch reversibly applied
tests Record the three newly served random corpus rows
tools test: keystroke evidence — rexpect PTY e2e (unix; Up recalls, Alt-. cycles, Ctrl-C survives #46) and the asked->bound->verified probe over every binding
vendor pin: re-vendor spec and corpus at wolf-lang v0.2.24 (294d626d) — two releases forward, an ancestor of the released line
.gitattributes seed: licenses, LF enforcement, readme
.gitignore ci: fmt/clippy/test/corpus/fixtures on three platforms plus an independence gate; conventions doc with the compiler track's platform lessons
.gitmodules org migration: tenseleyFlow -> wolffe-lang
CHANGELOG.md Describe entropy and the complete 0.1.56 delta
CONTRIBUTING.md CONTRIBUTING: CI and the slot budget after is78, tl29 and tl30b (the PR's own run is the gate; the footprints; #266)
Cargo.lock Bump lupin to 0.1.56
Cargo.toml Bump lupin to 0.1.56
LICENSE license: GPL-3.0-or-later (D41 as amended)
README.md Refresh the README version and corpus counts
build.rs build: stamp the host triple, LUPIN_HOST_TRIPLE (#182, #174)
rust-toolchain.toml is00: crate sources — omitted from the skeleton commit by an aborted pathspec batch
upstream @ 294d626 pin: re-vendor spec and corpus at wolf-lang v0.2.24 (294d626d) — two releases forward, an ancestor of the released line

wolf-interp

the wolf mark

The reference interpreter for the wolf language. It is an independent, executable reading of the specification, and the oracle the compiler (wolf-lang) is differentially tested against. The two implementations share the pinned spec, the pinned corpus and the observation protocol they are compared through, and no code. Wolf source files use the .lu extension. This repo builds a binary named lupin.

Licensed under GPL-3.0-or-later.

Install

brew tap wolffe-lang/wolf && brew install lupin     # builds from source
yay -S lupin-bin                                    # Arch, prebuilt (lupin builds)

Or an archive from releases. The GNU/Linux archives (x86-64 and aarch64) need glibc 2.35 or later (Ubuntu 22.04, Debian 12, Fedora 36+); 0.1.38's needed 2.39 and did not start on Ubuntu 22.04 (wolf-lang#447). The additional lupin-<ver>-x86_64-unknown-linux-musl.tar.gz archive is fully static: ET_EXEC, no ELF interpreter and no dynamic section. It needs no host libc, and can run on PAX's static ELF loader. Each release build checks that ELF contract and prints the binary's SHA-256 before archiving. Then:

printf 'fn main() {\n  print("hello, wolf")\n}\n' > hello.lu
lupin run hello.lu

Building from source

git clone https://github.com/wolffe-lang/wolf-interp
cd wolf-interp
cargo build --release

The binary lands at target/release/lupin, which is how the transcripts below spell it. The toolchain is pinned by rust-toolchain.toml. The spec and corpus come from a pinned wolf-lang checkout: the upstream/ submodule when it is initialized, otherwise the tracked snapshot under vendor/upstream/. A bare clone works without touching submodules (manual). --version names the binary's version and the upstream pin it was built against:

$ lupin --version
lupin 0.1.56… (wolf-interp, reference interpreter at pin …)

A build made at its release tag prints the bare version. Any other build, including the one above unless you checked out the tag, carries a +dev.<commit> suffix, so an off-tag build cannot be mistaken for the release (D57).

Running a program

examples/squares.lu, in full:

//! check: run(exit=0, stdout="sum of squares: 30")
//! phase: run

struct Point { x: int, y: int }

fn main() -> !int {
    var sum = 0
    region frame {
        var pts = List[Point]()
        for i in 0..5 { pts.push(Point { x: i, y: i * i }) }
        for p in pts { sum += p.y }
    }
    print("sum of squares: {sum}")
    0
}

Running the file takes no subcommand. The program's output passes through, and its exit(N) becomes the process exit code:

$ lupin examples/squares.lu
sum of squares: 30

Failures are reported in full. examples/overflow.lu overflows an i32. Arithmetic is checked in every build profile, so the program traps. The diagnostic goes to stderr and cites the spec clause it enforces, and the process exits 3:

$ lupin examples/overflow.lu
examples/overflow.lu: trap(overflow): `+` produced 2147483648, outside `i32` — checked arithmetic traps in every profile (X3); spell intended overflow `wrapping[i32]` [arith.checked] at 6:5

The exit codes are documented in the manual: the program's own exit(N), 2 on a static-phase rejection, 3 on a trap, 4 on unsupported. lupin - reads a program from stdin the same way.

The //! header is a conformance directive: the file states its own expected outcome, in the grammar the corpus uses. conform-run is the protocol surface. It runs the program and reports what it observed:

$ lupin conform-run examples/squares.lu
examples/squares.lu: verdict=exit(0) phase_reached=run seeded=false
sum of squares: 30

The first line is the observation: the verdict and the deepest pipeline phase that completed. The rest is the program's output.

The REPL

Bare lupin starts an interactive session, and lupin repl is the explicit spelling. Declarations persist, and a trap does not end the session. :mem, :regions and :trace show the memory model live. A walkthrough is in the manual. lupin eval 'CODE' (or -e) evaluates one snippet the same way and exits.

$ lupin
wolf> let s = "wolf"
wolf> let t = move s
wolf> s
trap(use-after-move): `s` was moved out and is uninitialized here [mem.tier0.move.2] at 0..1
  `s` moved here at 8..14
the session survives the trap; the world is as the fault left it [repl.trap.alive]
wolf> t
wolf : str
wolf> :quit

Commands

command what it does
run Run one program; output passes through live and the exit code is the program's (also lupin FILE.lu, no subcommand)
eval Evaluate a snippet in a fresh session and print its value as the REPL would
check Check files through the frontend only (lex, parse, resolve) and report diagnostics
repl Interactive session; --script replays a recorded transcript
conform-run Observe one program and emit a spec/06 observation record
corpus Walk the pinned corpus and check every directive against this implementation
lex, parse Run one frontend phase and dump its evidence
diff-run Compare this implementation against the pinned compiler, corpus-wide
conformance Export the versioned conformance bundle, or check an implementation against one
fuzz Differential testing over generated programs, with reduction of anything divergent
protocol Validate observation records against the spec/06 schema

A subcommand name wins over a file of the same name: a file literally named repl runs as lupin run repl. lupin <command> --help lists the flags; the manual covers each command with worked transcripts.

Scope

The interpreter implements the dynamic semantics in full, and only the static analysis needed to run programs. The type checker, borrow checker and region checker are the compiler's. Every property they prove statically is enforced dynamically here, so an ownership violation surfaces as a runtime trap where the compiler would refuse the program (manual). Of the 955 entry files in the pinned corpus, 763 reach the run rung. The corpus walk (lupin corpus) prints the exact ledger.

Documentation

docs/README.md is the index. User-facing material lives in docs/manual/. The engineering documents (the approximation contract, the divergence log, the bundle format) are beside it in docs/. Every command/output pair in this README and in the manual is real output from the pinned build, checked by a test. The spec is normative, and this implementation is one reading of it.

See CONTRIBUTING.md for the independence doctrine, the gates, and the commit conventions.