sysl

sysl.sys

The platform seam — every declaration in the standard library that is not sysl, in one file you cannot call.

sysl.sys is the one module in the standard library with no surface at all. Every name in it is private[sysl], so the whole of it is closed to a program:

import sysl.sys.sysl_putchar

print(sysl_putchar(104))
'sysl.sys.sysl_putchar' is private to module 'sysl'

That is not a page with nothing to say, though — it is the page about a seam. Every declaration in the standard library that is not sysl lives here, which means the surface a host has to supply can be read off two files, and the question “what would a freestanding target have to provide?” has a place to be answered.

Nothing in it is reachable, by any route

A fully-qualified path is refused for the same reason a bare one is. Being able to see a name is not being able to use it:

import sysl.sys.*

print(sysl.sys.sysl_sqrt(2.0))
'sysl.sys.sysl_sqrt' is private to module 'sysl'

And the glob import above is worth looking at twice, because it is not itself refused — it succeeds, and brings in nothing:

import sysl.sys.*

print(sysl_putchar(104))
undefined function 'sysl_putchar'

private[sysl] means the module sysl and its whole subtree, so every other part of the standard library may reach these and no program may. Putting a name here is therefore a decision about the library’s own workings rather than an addition to its surface — which is exactly what a reader wants a module like this to mean.

The sysl_ prefix, and what it buys you

Every extern here is bound to a sysl_-prefixed sysl name while going on resolving to the ordinary C symbol:

module sysl.sys
@link("m")

private[sysl] extern "putchar"   sysl_putchar(c: int) -> int
private[sysl] extern "llvm.sqrt" sysl_sqrt(x: f64) -> f64
private[sysl] extern "cbrt"      sysl_cbrt(x: f64) -> f64

An extern’s symbol is not qualified and cannot be. It names something the linker already has, and the linker knows nothing about sysl’s modules. So the sysl-side name is what had to move, and moving it is what keeps putchar, sqrt, pow, floor, read, memchr and strtod free for a program to declare itself:

import sysl.text.cstring

extern "putchar" putchar(c: int) -> int
extern "sqrt" sqrt(x: f64) -> f64
extern "strtod" strtod(p: *u8, end: **u8) -> real

putchar(104)
putchar(105)
putchar(10)

print(sqrt(2.0))

var s = cstring("3.5 and the rest")
var endp: *u8 = null
var v = strtod(s.ptr, &endp)

print(v)
hi
1.41421
3.5

Those three names are the program’s, bound to the same three symbols the library is bound to, and nothing collides. Spending seven ordinary words out of every program’s namespace would have bought nothing, and guide/fft had already declared its own sqrt before there was a module to ask.

The two halves

platform.sysl — what the library asks of the C library it is hosted on, and the whole of it:

symbolwhat the library uses it for
putcharevery byte print and prints emit
snprintfformatting a number into text
readsysl.io‘s FdReader, and stdin() under it
memchrfind_byte, and the line splitting built on it
strtodparse_real

math.sysl — what sysl.math asks of the machine and of the C mathematics library. Roots, exponentials and logarithms; powers and hypot; the circular and hyperbolic trigonometry; the four roundings; fmod; and the two sign-bit operations.

Two entry points per operation

C names the float widths apart, so sqrt takes a double and sqrtf a float, and both are declared. Overloading could give the pair one sysl name, and deliberately does not here: these are the raw declarations, and a name that did not match the symbol it resolves to is the one thing this module exists not to do. sysl.math is where the width stops being visible, and it stops there by dispatching on the receiver’s type rather than by a caller choosing which one they meant.

The intrinsics are spelled the same twice for a different reason: one base name, two widths, and the compiler derives .f64 or .f32 from the signature.

llvm. or libm — which is which, and why

Two kinds of declaration, told apart by the namespace the link name is in. A name beginning llvm. is an intrinsic: the back end recognises it and emits the machine’s own instruction, and there is no symbol for a linker to find. Everything else is libm’s, resolved at the link.

The split is not stylistic — it is which operations the hardware has. A square root, an absolute value, a sign transfer and the four roundings are instructions on every machine sysl targets. A sine is not, on any of them, so asking LLVM for llvm.sin would produce a call to the same libm function this file already names, one indirection later.

on the machinein libm
sqrt, fabs, copysigncbrt, exp, exp2, log, log2, log10
floor, ceil, round, truncpow, hypot, fmod
sin, cos, tan, asin, acos, atan, atan2
sinh, cosh, tanh, asinh, acosh, atanh

What that buys beyond speed is a program that needs no libc. A freestanding target has no libm to link against, so the whole module used to be hosted-only. The operations in the left column now work on a bare machine, and only the transcendentals in the right one do not.

round goes away from zero

llvm.round is C’s round, and it is not llvm.roundeven‘s rule. The two differ at exactly the inputs a rounding is chosen for, so the one adopted here is pinned by a test rather than assumed from the name:

import sysl.math.Float

print(2.5.round(), 3.5.round(), (-2.5).round())
print(2.5.trunc(), (-2.5).trunc(), 2.5.floor(), (-2.5).floor())
3 4 -3
2 -2 2 -3

Banker’s rounding would have answered 2 4 -2 on the first line. All four roundings answer in the float’s own type, because a floor that returned an integer would be undefined for the operands that do not fit one — and the caller who wants an integer is the one who knows the range.

Where the line is drawn

The transcendentals are here and most of the comparisons are not, and the line is what the machine can do that sysl’s operators cannot. A range-reduced sine is an algorithm. So, less obviously, are the two sign operations — because they read and write the sign bit directly, and a sign bit is something no comparison can see.

An absolute value written the obvious way is wrong, and here is the proof:

import sysl.math.Float

var z = -0.0
var hand = if z < 0.0 then -z else z

print(1.0 / z, 1.0 / z.abs(), 1.0 / hand)
-inf inf -inf

-0.0 < 0.0 is false — no comparison distinguishes a negative zero from a positive one — so the negation never runs and the magnitude comes back negative. abs clears the sign bit and gets it right. The only way to see the difference is to divide into it, which is what the program does.

copysign is on the same side of the line for the same reason:

import sysl.math.Float

print(3.0.copysign(-1.0), (-3.0).copysign(1.0), 0.0.copysign(-1.0) == 0.0)
print(1.0 / 0.0.copysign(-1.0))
-3 3 true
-inf

Read those two lines together. The third value on the first line says the result compares equal to positive zero; the second line says it is nevertheless a negative zero. That gap is the whole argument: a sysl body written out of comparisons cannot produce this value or detect it, so the operation has to be the machine’s.

Everything that is a comparison and nothing more — signum, is_finite, is_nan, the interpolation — sysl.math writes in sysl and calls nothing.

The module carries a link "m" directive, and the reason it names the library rather than the flag is that where libm lives is the target’s answer: a file of its own on ELF, part of libSystem on Darwin, absent from a freestanding machine.

The driver used to carry this instead, and every ELF link was handed -lm whether or not the program computed anything — because the compiler had no way to be told and this file had no way to say. Now the requirement travels with the declarations that create it.

Why it is a leaf

sysl.sys needs nothing. It imports no module, calls no sysl function, and reports no error — and that is a property worth protecting rather than an accident of how small it is.

The clearest illustration is a module that is not here. args_of converts C’s argc/argv into a []string, which sounds exactly like platform business — but it calls print and exit, which are sysl‘s, and sysl reaches sysl.sys for its printing. Putting both in one module would make the two depend on each other, which the acyclic module graph refuses. So it lives in sysl.args instead, and what is left here is a leaf.

A declaration that reports its own failure in words is not a leaf, because reporting is itself a dependency. That is the shape to look for when deciding whether something belongs at the seam.


That is the last module. Back to the section index for the tree, or to the language reference for what the compiler itself accepts.

Search

Esc
to navigate to open Esc to close