Skip to content

v0.4.0

Choose a tag to compare

@Ikepeeee Ikepeeee released this 15 Sep 13:05
· 133 commits to main since this release
8ebcc90

Where a command runs is now written down — or it does not run at all.

Until now a command execution expression with no $[...] fell back to the host. That fallback is the "works on my machine" failure the language exists to prevent: a deleted declaration or a mistyped program name went silent, and every line had to repeat the image it needed. v0.4.0 removes it, and gives the environment a place to be declared once.

Command declarations and explicit environments

  • command declares which image provides a program. command "python", "pip" on #python:3.12.14-alpine3.24 registers command words for the module; the tasks below then name only what they run: $ pip install -r api/requirements.txt && python -m unittest.
  • There is no default environment. An expression takes its environment from its $[...] or from the programs its command string invokes. One that determines neither is E-TYPE-COMMAND-NOENV, caught by lask check.
  • Dispatch decides only where the text decides. Spec 10.9 scans quoted and nested regions, separators and assignment words, and reports a command word only where the text alone determines it. Selection is unanimity over structural equality — #local included — so a line invoking both a host program and a containerised one is E-TYPE-COMMAND-CONFLICT, never a silent choice.
  • lask cmd runs a declared command by hand. lask cmd go test ./... runs the project's pinned toolchain outside any task, as an argument vector with no shell, attaching the terminal when there is one. Everything after the command name goes to the program verbatim, --help included; lask cmd --list shows what the module declares.
  • Six new error codes: E-TYPE-COMMAND-NOENV, -CONFLICT, -DECL, -NAME, -DUPLICATE, alongside the existing -ENV.

Shell completion

  • lask completion bash|zsh|fish prints a script whose only job is to ask the binary, so an installed script survives upgrades.
  • It knows your module, not just the CLI. lask run <TAB> completes the functions the entry module defines — less value bindings, @hidden, internal, and functions taking a positional Environment; lask run build --<TAB> completes that function's keyword parameters; lask cmd <TAB> completes declared commands. kebab and snake spellings mirror each other, and candidates come back in the style being typed.
  • Nothing is evaluated to answer a <TAB>. The index parses the entry module and stops there: no elaboration, no import resolution, no dependency cache, no network. Completion works before deps sync has ever run, and a module that does not parse still yields declaration heads. A request answers in ~22 ms, so there is no cache to go stale.
  • @complete steers a parameter (spec 3.1): static choices, @keys <binding> reading a top-level map literal without evaluating it, @file [*.ext], @dir.
  • lask run install_completion --shell fish writes the script into the directory that shell reads and prints the startup line rather than editing anyone's rc file. uninstall_completion is its counterpart.
  • The README now gives each shell a recipe that works on a stock machine — including that zsh loads nothing from $fpath until compinit has run, and that bash 3.2, still macOS's /bin/bash, silently reads nothing from source <(...).

In the editor

  • Semantic tokens paint a command word that carried the environment as the reference it is, inside the command string. Unmatched words stay string text, which makes a bracket-less command with nothing highlighted the visible form of E-TYPE-COMMAND-NOENV.
  • Inlay hints show the environment a bare command resolved to, written as the source the author could have written: $[#node:20.20.2-alpine3.23] cd web && npm ci. A line that already carries $[...] gets no hint, since it says where it runs.
  • The spans come from elaboration, so the editor shares dispatch's own table and procedure — a word that lights up is a word that voted, and no second, looser matcher exists to drift from it.
  • export and internal are surfaced like import: painted as keywords, offered by completion, and read by the token-stream fallback.

Language

  • Namespace-qualified type references. import * as tf from "terraform" now lets an annotation name that module's public type aliases directly: f(): tf.TfOutputs = .... Alias-cycle detection follows qualified edges across modules.
  • Every string and command chunk carries its source span, so anything downstream can name a location inside a command string.

Breaking changes

  • A command execution expression that determines no environment is a static error. Add a command ... on <Environment> declaration for the programs a module runs, or write $[...] at the site. The examples and main.lask in this repository show both.
  • run_command(cmd: String, env: Environment) takes the environment as a required positional argument, closing the path by which the core function could still default to the host.
  • export and internal are reserved words. A declaration, an identifier-form record field, or an unquoted object key by either name is now E-SYNTAX-UNEXPECTED-TOKEN; such a field is written {"internal": true} and read as x["internal"]. No module in this repository, lask-terraform or lask-aws was affected.
  • A command declaration's environment arguments must be literals: dispatch has to compare two environments structurally, and the environment must be enumerable and pinnable without evaluating the module.
  • Spec correction in 11.8: standard output is always passed through and only standard error is relayed as the command execution log — the previous wording would have made the output of lask cmd impossible to pipe.

Fixes and internals

  • stripSpansDecl had no DExportFrom case, so stripping spans from any module using export { .. } from ".." crashed at run time.
  • GHC 9.6.7 → 9.10.3 (Stackage LTS 24.58), with the build now warning-free: partial head uses replaced, shadowing binds renamed, dead code and unused imports dropped.
  • CoreProgram carries each module's command table, so lask envs and lask env build account for a declared command's image even when no task uses it.

Examples

  • example/04-webapp drives Terraform and AWS through the lask-terraform and lask-aws modules instead of hand-rolled command strings, and uses two official images — hashicorp/terraform and amazon/aws-cli — in place of a self-built combined Dockerfile, removing the build_infra_image() task and its "forgot to call it" failure mode. Exactly one line, test_e2e, still needs an explicit environment: it wants the Playwright image for its browsers rather than for the program it runs.
  • example/03-terraform bumps lask-terraform to v0.1.1.
  • The README says plainly what Lask does not replace: not GitHub Actions, GitLab CI or Jenkins, but what their jobs run — so switching providers rewrites one step rather than the pipeline.

Full Changelog: v0.3.0...v0.4.0