hd-lang

#Host

Status: standard library specification draft.

This chapter defines std.host, which lib/std writes in ordinary hd over the language tier:

  • the host capability traits Args, Env, and Sys;
  • the helpers args, env, env_as, and env_decode, and the shared HostError;
  • ScriptedArgs, ScriptedEnv, and ScriptedSys, their deterministic providers;
  • what every host operation does while it waits, when it is cancelled, and with the data it holds.

Which provider a command binds for each trait is CLI tier (Host Capabilities).

#Program Arguments

Args reads the arguments the program was started with:

pub trait Args:
    fn program(self) -> string
    fn list(self) -> List[string]
  1. std-host.args.declstd.host declares the host capability trait Args with the methods above. Code imports it, as in use std.host.Args.
  2. std-host.args.programprogram returns the name the program was started with.
  3. std-host.args.listlist returns the program's arguments, in order, without the program name.
  4. std-host.args.helperstd.host declares pub fn args() -> List[string] $ Args, which returns list() of the Args provider that covers the call.

#Environment

Env reads environment variables:

pub trait Env:
    fn get(self, name: string) -> string?
    fn names(self) -> List[string]
  1. std-host.env.declstd.host declares the host capability trait Env with the methods above. Code imports it, as in use std.host.Env.
  2. std-host.env.getget(name) returns the value of the variable name, or .None when it is not set.
  3. std-host.env.namesnames returns the name of every set variable, each once.
  4. std-host.env.helperstd.host declares pub fn env(name: string) -> string? $ Env, which returns get(name) of the Env provider that covers the call.

Note. Under an Env grant, the default profile's get returns .None for a variable outside the grant, and names leaves it out (Environment Grant).

#Typed Variables

env_as reads a variable as any type that implements Parse:

use std.host.{Env, HostError, env_as}

fn workers() -> Result[usize, HostError] $ Env:
    .Ok(env_as::[usize]("WORKERS")?.unwrap_or(4))
pub enum HostError:
    NotGranted(subject: string)
    Unsupported(name: string)
    Other(message: string)
    InvalidValue(name: string, reason: string)
    Missing(name: string)
  1. std-host.env-as.shared-error-signaturestd.host declares pub fn env_as[T < Parse[Error = E], E < Display](name: string) -> Result[T?, HostError] $ Env. The call names the type, as in env_as::[usize]("WORKERS").
  2. std-host.env-as.unsetWhen env(name) is .None, env_as returns .Ok(.None).
  3. std-host.env-as.presentWhen env(name) is .Some(text) and T::parse(text) returns .Ok(value), env_as returns .Ok(.Some(value)).
  4. std-host.env-as.invalid-host-errorWhen T::parse(text) returns .Err(error), env_as returns .Err(HostError.InvalidValue(name, reason)). reason is the Display text of error.
  5. std-host.error.typed-env-declstd.host declares HostError with the five variants above.
  6. std-host.env-as.plainenv_as is a plain call, like env.
  7. std-host.error.traitsHostError implements Eq, Debug, Display, and Error.
  8. std-host.error.invalid-value-displayInvalidValue(name, reason) displays invalid value for NAME: REASON.
  9. std-host.error.not-granted-displayNotGranted(subject) displays sys access to SUBJECT is not granted.
  10. std-host.error.unsupported-displayUnsupported(name) displays sys access to NAME is not supported.
  11. std-host.error.other-displayOther(message) displays message unchanged.

Why. An unset variable is normal, so it is .Ok(.None), as Go's os.LookupEnv reports it apart from an empty value. A set variable that does not parse is a mistake the program should show, so it is an error that names the variable, as strconv errors name the text.

#Typed Records

env_decode reads a record through the existing Deserialize protocol.

use std.host.{Env, HostError, env_decode}
use std.serde.Deserialize

@derive(Deserialize)
struct Config:
    listen_port: u16 = 8080
    optional_label: string?

fn config() -> Result[Config, HostError] $ Env:
    env_decode::[Config]()
  1. std-host.env-decode.declstd.host declares pub fn env_decode[T < Deserialize]() -> Result[T, HostError] $ Env.
  2. std-host.env-decode.record-rootThe root value must be a struct with named fields.
  3. std-host.env-decode.root-errorA non-record root returns HostError.Other("env_decode needs a struct with named fields").
  4. std-host.env-decode.field-nameEach field's environment name defaults to its source name converted to uppercase.
  5. std-host.env-decode.name-factstd.host declares the non-generic field fact EnvName with the public field name: string.
  6. std-host.env-decode.name-constructorstd.host declares pub fn env_name(name: string) -> EnvName, which returns EnvName { name: name }.
  7. std-host.env-decode.override-nameA field's EnvName.name, written as @env_name("REGIONS"), replaces its default environment name.
  8. std-host.env-decode.names-independentEnvironment names are independent of serde's document-name facts.
  9. std-host.env-decode.empty-nameAn empty EnvName.name returns HostError.InvalidValue(field, "empty environment name"), where field is the source field name.
  10. std-host.env-decode.empty-name-no-readAn empty EnvName.name must be rejected before reading that environment variable.
  11. std-host.env-decode.declared-readsDecoding reads only the fields' selected names through Env.get, without calling Env.names.
  12. std-host.env-decode.unset-nullAn unset variable is an absent, null value to the deserializer.
  13. std-host.env-decode.optionalWithout a declared default, an unset variable for an optional field becomes .None.
  14. std-host.env-decode.defaultA field whose variable is unset uses its declared default when one is present.
  15. std-host.env-decode.missingAn absent required field without a declared default returns HostError.Missing(name), including when the field is a list.
  16. std-host.env-decode.scalarScalar text is read through the deserializer's bool, int, uint, float, and text methods.
  17. std-host.env-decode.list-separatorA list field splits its variable's text on commas.
  18. std-host.env-decode.list-no-trimSplitting a list does not trim its items.
  19. std-host.env-decode.list-emptyAn explicitly empty variable for a list decodes as [].
  20. std-host.env-decode.invalid-valueA refused variable value returns HostError.InvalidValue(name, reason), naming the variable and the refusal reason.
  21. std-host.env-decode.custom-deserializeA field's custom Deserialize implementation reads through the same environment deserializer.
  22. std-host.env-decode.plainenv_decode is a plain call.
  23. std-host.error.missing-displayMissing(name) displays missing environment variable NAME.

Why. Environment and document formats share typed deserialization, while separate field facts keep environment variable names independent.

#Plain Reads

  1. std-host.plain-readsEvery method of Args and Env, and both helpers, are plain calls, not bang calls.
use std.host.{Args, Env, args, env}

fn greeting() -> string $ Args + Env:
    let names = args()
    match env("GREETING"):
        .Some(word) => "${word}, ${names.len()}"
        .None => "hello"

Why. An argument or environment read returns a value the host already holds, so it needs no driver. Replay records it at the boundary either way.

#Map Providers

ScriptedArgs and ScriptedEnv are the deterministic providers of Args and Env. A test supplies them with $.with:

use std.host.{Args, Env, ScriptedArgs, ScriptedEnv, args, env}
fn greeting() -> string $ Args + Env:
    match env("GREETING"):
        .Some(word) => "${word}, ${args().len()}"
        .None => "hello"

tests:
    use std.testing.assert_equal

    it("greets from fixed input"):
        given := ScriptedArgs::new("tool", ["a", "b"])
        vars := ScriptedEnv::new({"GREETING": "hi", "HOME": "/home/me"})
        $.with(Args=given, Env=vars):
            assert_equal(greeting(), "hi, 2", reason="one variable, two arguments")
        assert_equal(vars.names().sorted(), ["GREETING", "HOME"], reason="both keys")
  1. std-host.map-args.declstd.host declares ScriptedArgs, which implements Args, with private fields. Code imports it, as in use std.host.ScriptedArgs.
  2. std-host.map-args.newScriptedArgs::new(program: string, values: List[string]) -> ScriptedArgs returns a provider whose program returns program and whose list returns values.
  3. std-host.map-env.declstd.host declares ScriptedEnv, which implements Env, with private fields. Code imports it, as in use std.host.ScriptedEnv.
  4. std-host.map-env.newScriptedEnv::new(values: Map[string, string]) -> ScriptedEnv returns a provider whose get(name) returns the value of the key name in values, or .None when there is no such key.
  5. std-host.map-env.names.ordernames on a ScriptedEnv returns the keys of values in the map's iteration order.
  6. std-host.map.no-hostNeither provider reads the host's arguments or environment.

Why. A test states every argument and variable that the code under test reads, so nothing leaks in from the machine that runs it. Effect's ConfigProvider.fromMap serves tests the same way.

#System Reads

Sys reads four facts about the host, as Go's runtime.GOOS, runtime.GOARCH, runtime.NumCPU, and os.Hostname do:

pub trait Sys:
    fn os(self) -> Result[string, HostError]
    fn arch(self) -> Result[string, HostError]
    fn hostname(self) -> Result[string, HostError]
    fn cpu_count(self) -> Result[u32, HostError]
  1. std-host.sys.declstd.host declares the host capability trait Sys as above. Code imports it, as in use std.host.Sys.
  2. std-host.sys.osos returns the operating system's name in lowercase, such as linux, macos, or windows.
  3. std-host.sys.archarch returns the host machine's architecture, such as x86_64 or aarch64.
  4. std-host.sys.hostnamehostname returns the host's name.
  5. std-host.sys.cpu-countcpu_count returns the number of logical processors that the host offers the program.
  6. std-host.sys.plainEvery method of Sys is a plain call, not a bang call, as the reads of Env are by std-host.plain-reads.
  7. std-host.sys.not-grantedA method returns .Err(HostError.NotGranted(subject)) when the program's capability grant does not cover it, with the method's name for subject.
  8. std-host.sys.unsupportedA method returns .Err(HostError.Unsupported(name)) when the host can't answer it, as a browser can't for hostname.

Why. The grant covers each read by its method's name, as Deno gates each sys call, so each method returns a Result.

#Map Sys

ScriptedSys is the deterministic Sys provider. A test supplies it with $.with:

use std.host.{ScriptedSys, Sys, HostError}
fn platform() -> string $ Sys:
    match $.use(Sys).os():
        .Ok(name) => name
        .Err(_) => "unknown"

tests:
    use std.testing.assert_equal

    it("reads the mapped system"):
        $.with(Sys=ScriptedSys::new({"os": "linux", "cpu_count": "8"})):
            assert_equal(platform(), "linux", reason="the mapped os")
            assert_equal($.use(Sys).cpu_count(), .Ok(8), reason="parsed as a count")
            assert_equal($.use(Sys).hostname(), .Err(HostError.Unsupported("hostname")), reason="not in the map")
  1. std-host.sys.map.declstd.host declares ScriptedSys, which implements Sys, with private fields. Code imports it, as in use std.host.ScriptedSys.
  2. std-host.sys.map.newScriptedSys::new(values: Map[string, string]) -> ScriptedSys returns a provider whose methods each return the value of the key that is the method's name.
  3. std-host.sys.map.cpu-countcpu_count reads its value as a decimal u32.
  4. std-host.sys.map.missingA method whose key is missing, or whose value cpu_count can't read, returns .Err(HostError.Unsupported(name)).
  5. std-host.sys.map.no-hostA ScriptedSys never reads the host system.

#Host Operations

A host operation is a bang method call that the host completes, such as send! on the host's Http provider. While one waits, the instance runs its other tasks:

use std.http.{Http, HttpError, Response, get}
use std.task.all

fn both!(left: string, right: string) -> (Result[Response, HttpError], Result[Response, HttpError]) $ Http:
    all!(get(left), get(right))   # both requests wait at the same time
  1. std-host.op.waitA bang host method that waits lets the instance run its other tasks until it completes.
  2. std-host.op.no-orderIndependent host operations that complete together have no defined order, so the instance may resume their tasks in any order.
  3. std-host.op.cancelCancelling the suspension of a pending host operation ends that operation. Each trait states what the operation leaves behind, as the table below lists.
  4. std-host.op.cancel.writeA cancelled host operation that writes may have happened, in whole or in part.
Trait A cancelled operation leaves
Process no child: it and its process group are killed, by std-process.cancel
Http no connection: it is closed, by std-http.send.cancel.close
Net the bytes not yet returned, for the next read, by std-net.cancel.read
FsWrite possibly a partial file, by std-fs.cancel.write

Why. A race! timeout around a host call is portable only when the call lets the timer run. Completions that arrive together may reach the instance in one batch, so no program may rely on their order.

See also: Program Arguments, Entry Arguments.