Skip to content

Releases

sift 1.31.0 — the builtins are code now

Choose a tag to compare

@jjba23 jjba23 released this 14 Nov 11:09
ae9b6ce

The second half of the standard library — the names an implementation compiled
in — used to be a file of prose. It is source now, and Racket is read the way
its own reader reads it.

A jump to car lands in code. car is in libguile and no installed file
defines it, so sift asks the implementation once and writes the answer as a
file in the dialect it describes: a procedure becomes a define or defun with
its docstring, a macro a define-macro or defmacro, and a value a define or
defvar — or, in Scheme, a define documented by the comment above it, because
Scheme has no docstring slot for a value.

;; sift --- the names GNU Guile 3.0.9 documents about itself
;; sift: version 3.0.9

(define (log10 x)
  "Return the base-10 logarithm of x."
  #f)

That file is read by the same reader as everything else, so a jump lands in code
and the hover is the hover of any other definition. A hover also falls back to
the comment above a definition that carries no docstring, which is how a Lisp
has always documented a value.

Actual code comes first. Every definition site is ranked, and the generated
file is last: a project, a required library and the installation's own sources
are offered before it. Against a real Guile, car resolves to srfi-17.scm
and map to srfi-1.scm, base.scm or boot-9.scm; log10 — which only the
compiled implementation defines — resolves to the generated file. The bespoke
### index parser is gone, and the cache version is bumped with the format.

Racket, read as Racket. Twelve things the first Racket release read as if
it were Scheme:

  • a backslash escapes the next character of an identifier, so foo\( is one
    name rather than an unterminated list;
  • #lang may follow the shebang that makes a module executable;
  • {...} is another spelling of (...) — a list, kept as written, and a quoted
    brace list stays inside its quote;
  • #u8(...) is Scheme's bytevector; a Racket file now says so instead of
    reading it silently;
  • :height is an identifier in Racket, and foo:bar carries no package marker;
  • .scrbl is Scribble, whose at-expression reader sift does not implement, so
    it is no longer claimed as Racket or offered to the formatter and editors —
    collapsing @verbatim{a b} is corruption;
  • a dialect marker counts only where a directive can be written, not in a
    comment or a string;
  • internal-define is Scheme's rule: Racket allows a definition interleaved
    with expressions in a body;
  • a case key and a match/match* subject are expressions, so
    clause-brackets reads the clauses after them;
  • (match (build-value a b) [p r]) brackets the clause, not the call around the
    subject, and a syntax-case literals list is not a clause;
  • a match pattern is data all the way down, except where ?, app and ==
    evaluate a sub-expression — an if-without-else fix can no longer rewrite a
    macro pattern;
  • lint --checker NAME respects the dialect the checker is for, as its
    docstring always promised.

The suite is guarded. A test written inside another test never runs, and
the run is green anyway. One was: a missing parenthesis made a test part of
the test above it, and two assertions passed by never executing.
tests/suites.scm now reads every test file with sift's own reader and fails
when a test is not a direct child of a suite.

Full Changelog: v1.30.0...v1.31.0

sift 1.30.0 — Racket, and what a definition costs

Choose a tag to compare

@jjba23 jjba23 released this 14 Nov 09:43
aeb96eb

Racket is the fourth dialect, and the first release that indexes a standard
library sift did not ship a program for.

Racket is read by its own report. A .rkt, .rktd, .rktl or .scrbl
file, a shebang naming racket, or a #lang line is a Racket module; the
#lang line is a node of its own, so the formatter keeps it whole and puts the
blank line after it that a Racket module has. #:height is a keyword rather
than an uninterned symbol, and #hash, #rx"...", #s(posn ...) and #&box
keep the datum glued to the reader prefix that produced it. A square bracket is
a cond or match clause, and #{...}# — which only Guile reads — is reported
as the syntax it is not. The rules whose premise is another dialect's
(unused-import, call-arity, case-literal-datum, negated-if,
docstring-texinfo) do not run on a Racket file, because a finding a reader has
to argue with is worse than no finding at all. Formatting, linting, the
language server, the editor integrations and the browser build all take
racket as a dialect.

A Racket standard library, read from source. sift lsp no longer answers
"no file defines this" for every name in racket/base. Racket ships collects
and pkgs as source, so the index is read the way Guile's is: the installation
the racket on the path belongs to, the version trees a --user package went
into, and whatever PLTCOLLECTS names. No Racket program is shipped to
describe it. The name that only the compiled #%kernel defines is the one it
cannot answer, and it answers that as a file that does not exist rather than as
a guess. struct is a defining form, so the outline, rename and inlay hints
see one.

Two rules about what a definition costs. SIFT094 cognitive-complexity
charges one for a branch and one more for each branch inside it;
SIFT095 cyclomatic-complexity counts the paths a test would have to cover.
Both are info, and both budgets are settings — max-cognitive-complexity (15)
and max-cyclomatic-complexity (10) — because a program that spawns processes
for a living may nest more than one that formats a buffer. They read the
measurement maak complexity prints, from (sift complexity), so a finding and
the report cannot disagree about the same function. Moving that measurement
fixed one thing it had already claimed: a quoted datum is data, so
'(if a (cond [b 1])) is no longer three branches. A cond stays one decision
rather than one per clause, which is what the report has always counted.

The catalogue is 95 rules. Nothing that worked before stops working: the two
rules are info, so a project at severity: warning neither shows them nor
fails on them, and a project that wants them says so with a budget.

Full Changelog: v1.29.2...v1.30.0

sift 1.29.3

Choose a tag to compare

@jjba23 jjba23 released this 13 Nov 23:00
e89c077

v1.29.3 (2026-11-13)

A documentation release. Nothing in the reader, the formatter, the rules, the language server or the browser build changed.

Documentation

  • A vision document. VISION.md argues that the style should become a specification with a conformance corpus, so a second implementation can be checked rather than merely read.
  • It names the largest correctness gap and a plausible answer: a declared macro table — indentation, arity, which arguments are data, which are body — in the shape Emacs has kept for indent and edebug specs.
  • It asks for resolution across files, following define-module, #:use-module, defpackage and provide, so a reference is a binding rather than a name and a project-wide rename is correct everywhere.
  • In-scope items are documented refusals read against the request table: range formatting and fold ranges, pull diagnostics beside the pushed ones, and a fourth dialect whose every rule and fixture has to be re-argued.
  • New ideas in the tool's character: sift new-rule, range-limited analysis in the browser tab, a --since REV mode with a policy for new versus existing findings, and a complexity gate built on etc/complexity.scm.
  • It compares sift with cljfmt, clj-kondo, rustfmt and gofmt, clang-format, Prettier, Emacs's own elisp toolchain, scheme-mode, and SLIME and SLY — and says what must stay: the fixed point asserted over every fixture, the rule catalogue as data, the documented refusals, and the browser build as a compile of the same modules.

Verification

Documentation only, so no behaviour was exercised: this adds one file and touches no source, rule, fixture or build path. The tree was clean before the commit and is clean after it.


Release prepared with ECA; Josep Bigorra is author of record.

sift 1.29.2 — the loop shapes nobody had checked

Choose a tag to compare

@jjba23 jjba23 released this 13 Nov 19:08
1fea9ed

Three things were left unpinned after 1.29.1: a dangling-style project, the
VS Code extension, and a loop clause whose body starts its own line. All three
are now checked against something rather than assumed.

A clause body on its own line was already right, in both dialects, and the
two engines that say so agree with sift column for column:

(loop for x in xs
      do
      (message "%s" x))

emacs-lisp-mode puts do and the forms after it in the column of for, and
cl-indent — what SLIME indents with — does the same for :do and :for. A
unit test in each dialect and a fixture pair for each corpus pin it, and the
expected files are editor output.

The dangling style keeps a loop's clauses in one column and its closer
aligned with the opener that opened it, and reformatting changes nothing. That
is a new fixture under tests/fixtures/elisp/dangling — the style was covered
for Scheme only.

The VS Code extension has no indentation of its own: "Format Document" runs
sift format --stdin, so it inherits the clauses rule from the executable and
handles a loop correctly by construction, with no change needed. It does not
pass --style and does not run with the document's directory as its working
directory, so a project that sets style: dangling or its own indent-rules in
sift.yaml is formatted with the defaults there. That is not fixed here; it
belongs in its own change, and the fix is one cwd rather than a second
setting.

And the guide stopped lying about an editor. It said Emacs indents a loop's
clauses two columns in when the opening line names none. It does not:
emacs-lisp-mode moves them one column in from the opener, cl-indent keeps
them under the column a first clause would have had, and a pcase with nothing
after it is four in. sift indents two columns in — one answer for every clause
list with no clause on its opening line, and a fixed point of its own
formatting — and the guide now says so instead of claiming agreement nobody
has.

No behaviour changes: formatting, linting and the language server answer exactly
what 1.29.1 answered.

Full Changelog: v1.29.1...v1.29.2

sift 1.29.1 — a loop is clauses, and Emacs was right

Choose a tag to compare

@jjba23 jjba23 released this 13 Nov 16:57
247c45b

Issue #9, all three findings, from one fact that sift only had in the
formatter: a form whose elements are clauses is not a call with arguments.

Formatting. loop was indented two columns in from the opener in both
Emacs Lisp and Common Lisp, where Emacs lines every clause up under the first
one. The clauses rule that cond already used now applies to loop too:
for, with and do sit in one column, and a (loop with nothing after it
still indents its clauses two in.

A file formatted by an older sift is reformatted once:

sift format --write

SIFT009 (duplicate-keyword) no longer reports a loop: every keyword of
a loop starts a clause, so :of-type written twice declares the type of two
different variables, and two :do clauses both run. A duplicate inside a
loop's clause is still a duplicate.

SIFT031 (wrap-in-let) no longer counts a clause list as one long
expression. A many-clause cond had the same problem and is fixed with it;
the depth rule still applies to a clause.

The one predicate behind the two checks is clause-headed?, exported from
(sift format) beside the clause-taking-forms table it belongs with, so a
clause-shaped form added there is understood by the formatter and the checkers
at once. SIFT009 had no test suite until now; it has one.

Full Changelog: v1.29.0...v1.29.1

sift 1.29.0 — a reusable workflow that needs no Guix

Choose a tag to compare

@jjba23 jjba23 released this 13 Nov 16:35
d0426f1

The reusable workflow in .github/workflows/sift.yml no longer installs Guix.
install now defaults to guile: the job runs apt-get install guile-3.0 on
the runner and runs the checked-out sift as it stands, because sift is source
and a launcher and there is nothing to build. On a cold ubuntu:24.04 over
src/ (36 files) that measured 32s for the package install, 22s for Guile to
compile the library on the first run, and then 3.5s and 3.3s for the two
passes — where the Guix step it replaces bootstrapped a daemon and built the
checkout before reading a file.

The composite action gained the same mode: install: guile runs the action's
own checkout with the guile on the PATH, and auto prefers it over building.
install: guix and install: nix still build the checkout into a store path
for a caller that wants that, and install: path still uses a sift an earlier
step provided.

Nothing needs to change in a caller: the inputs mean what they meant and the
annotations and the SARIF document are the same. A caller that wants the old
behaviour writes with: install: guix.

Full Changelog: v1.28.1...v1.29.0

sift 1.28.1

Choose a tag to compare

@jjba23 jjba23 released this 13 Nov 06:49
561c945

Full Changelog: v1.28.0...v1.28.1

sift 1.28.0

Choose a tag to compare

@jjba23 jjba23 released this 13 Nov 06:26
640d8ce

Full Changelog: v1.27.3...v1.28.0

v1.27.3 — the channels at their latest releases

Choose a tag to compare

@jjba23 jjba23 released this 12 Nov 15:56
08c5bdf

Summary

  • channels.scm: the jointhefreeworld collection at v1.0.4 (commit 872c1e2) and Guix master 3b4e873.
  • flake.lock: every input re-locked against nixos-unstable, evaluated with nix flake check --no-build.
  • Version statements: .github/workflows/sift.yml, .pre-commit-hooks.yaml, action.yml, doc/editors.org, doc/rules.org, editors/vscode/package.json, emacs/sift.el, src/sift/records.scm.

A dependency release: no code changed, so the suites that were green before this tag are the suites this tag ships.

Verification

  • guix time-machine --channels=channels.scm -- describe resolves every channel this repository pins.
  • nix flake check --no-build evaluates the flake.

Notes

  • Agent involvement: ECA (deepseek/deepseek-flash) moved the pins and cut this release under the maintainer's direction.

🤖 Generated with ECA (deepseek/deepseek-flash)

sift 1.27.2 — the browser build compiles again

Choose a tag to compare

@jjba23 jjba23 released this 12 Nov 14:32
9ef9819

Fix

  • The browser build compiles again. The deploy job of v1.27.1 was the first thing to build the Wasm module since the index work landed, and it stopped at make-deadline: get-internal-real-time and internal-time-units-per-second are (guile) names that the target's (guile) does not bind. A core that compiles under Guile therefore stopped the browser build by name half way through — the failure mode web/README.org describes, found by the deploy job rather than by a test.
  • The clock is R7RS's (scheme time) now. current-jiffy and jiffies-per-second are the portable spelling of the same question, monotonic, and answered by both implementations without a shim. (ice-9 time) and (srfi srfi-19) were the obvious alternatives and neither works: both reach for the same missing names. Nothing else changes — a deadline is still a moment, and the bound is still kept by not starting work that would pass it.
  • The comment says it without naming a target. tests/web.scm keeps the seam (no file under src/sift/ may name the browser, Wasm or Hoot), and a comment is a file's text: the first attempt broke that test, which is the test doing its job.

Continuous integration

  • maak-web moved into the gates. It was only ever run by deploy, which runs after the merge, so the class of failure no Guile-side test can see — a name the Wasm target lacks — reached the trunk. The gates compile the browser build on the pull request as well as on the push now, and it costs a compile the deploy job was paying for anyway.

Verification

  • maak release 1.27.2 ran every gate before the commit: docs, test, check, self-check, fixture-check, emacs-check, emacs-test.
  • Veritas suite: 105 tasks, 153 seconds, no failures. ERT suite: 31 tests, 0 unexpected.
  • sh web/build.sh writes sift.wasm (3.1 MB) and the page's three requests — version, lint, format — answer through Hoot's runtime under Node, so the module loads and runs, not only compiles.
  • make-deadline still behaves: a 100 ms deadline is in the future, a 0 ms one has passed.

🤖 Generated with ECA (deepseek/deepseek-flash)