Swift with no standard library

Is it possible to configure Xcode / command line tools to run bare swift with no standard library, or is it too much hassle / impossible?

I'm not a compiler contributor, so I may be wrong, but seeing as many of the fundamental types in Swift (String, Int, Bool, etc.) are part of the standard library, I doubt you'd be able to do much of anything without it.

4 Likes

What do you mean by "with no standard library", exactly? E.g. it's easy enough to pass -parse-stdlib which prevents the stdlib from being automatically imported. Can you provide a little bit more detail about what you're trying to do?

2 Likes

Wow, that sounds easy even for me :) will try that.

Just want to see what's available in bare swift without standard library (which seems to be more invasive compared to C/C++ standard library). e.g. if this compiles:

func foo() {}

all good, and if this doesn't:

func foo() -> Int {}

I'd know that "Int" lives in the standards library, and so on.

Right. Example: Compiler Explorer

Note that what you can do without the standard library is extremely limited. You can build types on top of LLVM builtin types (e.g. Builtin.Int32), but it's difficult to do much more than that.

3 Likes

Is it possible for a code outside of the standard library to access LLVM builtin types? If yes, how?

Yes, by passing -parse-stdlib (and then importing Swift to get the standard library). But there is generally no reason to do so; the stdlib's Int32 type is strictly more useful than Builtin.Int32 is. It occasionally is useful to use LLVM intrinsics or operations, but in those cases the best thing to do is to get the operation you need added to the stdlib.

e.g. in a hypothetical world where the wrapping &+ operator didn't exist, you could build it using Builtin.add: Compiler Explorer

Note that the Builtin module's documentation is just the LLVM language reference, and there's a little bit of an art to figuring out how to map an LLVM operation or intrinsic into a Swift builtin function. The learning curve is kinda steep, and the diagnostics are not especially helpful. Looking at examples from the standard library source is informative. It's also worth noting that many LLVM operations have C bindings via either platform intrinsics or builtin functions. If you're using something that can be represented that way, it's usually best to write a small .h file that wraps what you need into a module and import that in your Swift code, as that's much easier to use than the raw Builtin module.

6 Likes

Important to note, -parse-stdlib is used for just that, building the standard library. It's not strictly equivalent to, for example, Rust's no_std. I'm guessing, there's high probability you may want to access arbitrary memory, as stack allocations won't be enough. For that you'll need pointers, and all useful pointer types are declared in stdlib.

4 Likes

If you didn't want to use LLVM builtins either, I believe the only way to have a type with different values is to use an enum. For instance, you could recreate Bool:

enum Bool {
  case true
  case false
}

From that, you can start to create structs/tuples containing bools. You could use it to create a binary integer type, implement addition, etc.

It wouldn't be as good as using the LLVM builtins (and unless we can guarantee a struct with 8 bools are packed in a single byte, some code might not work at all), but it might be fun to see how far you can go, just starting out with Bool.

3 Likes

We do not really maintain a clean layering between the compiler, runtime, and standard library, so they all tend to be a little codependent, and if you try to work without the standard library or build another one from first principles, you're going to waste a lot of time dealing with weird compiler errors and crashes and cutting and pasting definitions from the real standard library just to get things working. We've recently started working on a "standalone" mode for building the standard library that omits some of the bulkier runtime dependencies, and I think that's a better way to go forward—organizing the standard library into components, and allowing some of them to be disabled for limited platforms—rather than try to maintain alternative implementations of the standard library. Historically, multiple standard libraries has not worked well for other language ecosystems; D 1.0 and OCaml come to my mind as great languages whose adoption was hobbled by their communities splintering into multiple incompatible standard library camps.

24 Likes

Great responses, thank you.

I was extremely surprised to know that Int and Bool are not part of swift itself.
And indeed there would be a learning curve down this road, e.g.:

this is fine:

func foo(a: Any) {}

but (expectedly) this is not:

func foo(a: Any...) {} //  🛑 error: broken standard library: cannot find Array type

although (unexpectedly) this doesn't help:

struct Array {}

ditto here:

let x = 0 // 🛑 error: missing protocol 'ExpressibleByIntegerLiteral'
protocol ExpressibleByIntegerLiteral {} // <-- this doesn't help

but at least I can assign things one to another!

var x: () = ()
var y: () = ()
x = y

that of course only after I add the missing precedence group:

precedencegroup AssignmentPrecedence {}

as if there is already built-in "operator =" without the corresponding built-in AssignmentPrecedence.

and so on.

Then I'd make a giant global var and create my allocator to feed from that var payload :slight_smile:

great idea for a hackaton. "Look, I started from enum Bool { case false, true } now look at this fully functioning (yet a bit slow) iOS app / HTTP server / whatever". Exaggerating of course.

Here's a little gotcha I found the last time I tried this little thought experiment:

// This is fine, the compiler optimizes it almost exactly like Bool
enum Bit { case zero, one }
// This monstrosity is 8 bytes large.
struct Byte {
    var bits: (Bit, Bit, Bit, Bit, Bit, Bit, Bit, Bit)
}

The correct approach in this case is probably more like:

enum Byte {
    case zero, one, two, ..., two-hundred-fifty-six
}

Although implementing FixedWidthInteger with just enums and tuples as a base is going to a pain regardless of how you do it.

1 Like

My first thought was to use associated values:

enum OneMoreBit<T> {
    case zero(T)
    case one(T)
}

typealias Byte = OneMoreBit<OneMoreBit<OneMoreBit<OneMoreBit<OneMoreBit<OneMoreBit<OneMoreBit<OneMoreBit<()>>>>>>>>

but, to my surprise, that seems to also be eight bytes. Can Swift not optimize the size of nested enums?

We don't support spare bit optimization for generic types. Eventually, we want to do automatic bit-packing of boolean and small enum fields in structs.

4 Likes

BTW, false & true are treated as keywords:

enum Bool {
  case false // 🛑 keyword 'false' cannot be used as an identifier here
  case true  // 🛑 keyword 'true' cannot be used as an identifier here
}

Although there are not fully available:

let x = true // 🛑 missing protocol 'ExpressibleByBooleanLiteral'

That's what backticks are for.

enum Bool {
    case `false`, `true`
}
1 Like

Very interesting thread.
I'm surprised that we can never use optional bindings without stdlib...

// with '-parse-stdlib'
enum Optional<Wrapped> { case none, some(Wrapped) }
struct S {}
let myOptional: Optional<S> = .some(S()) // OK
let sugaredOptional: S? = .some(S()) // ⛔️ error: broken standard library: cannot find Optional type

if let v1 = myOptional {} // ⛔️ error: type of expression is ambiguous without more context
if let v2 = sugaredOptional {} // ⛔️ error: type of expression is ambiguous without more context
if let v3: S = myOptional {} // ⛔️ error: cannot convert value of type 'Optional<S>' to specified type 'S?'
if let v4: S = sugaredOptional {} // ✅ but error above.

What happens if you change your module name to Swift ?

Oh, that's it.
The linker fails, though. :sweat_smile:

1 Like