{"id":50019983,"url":"https://github.com/skx/s-lang","last_synced_at":"2026-06-07T12:03:17.361Z","repository":{"id":356369672,"uuid":"1231518684","full_name":"skx/s-lang","owner":"skx","description":"linux/amd64 compiler for simple language","archived":false,"fork":false,"pushed_at":"2026-06-06T18:32:41.000Z","size":409,"stargazers_count":2,"open_issues_count":1,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-06-06T20:14:03.897Z","etag":null,"topics":["assembly-language","assembly-x86","compiler","golang","linux","programming-language"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"gpl-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/skx.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-07T03:18:48.000Z","updated_at":"2026-06-06T18:32:43.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/skx/s-lang","commit_stats":null,"previous_names":["skx/s-lang"],"tags_count":8,"template":false,"template_full_name":null,"purl":"pkg:github/skx/s-lang","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/skx%2Fs-lang","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/skx%2Fs-lang/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/skx%2Fs-lang/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/skx%2Fs-lang/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/skx","download_url":"https://codeload.github.com/skx/s-lang/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/skx%2Fs-lang/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34020187,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-07T02:00:07.652Z","response_time":124,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["assembly-language","assembly-x86","compiler","golang","linux","programming-language"],"created_at":"2026-05-20T07:20:51.271Z","updated_at":"2026-06-07T12:03:17.348Z","avatar_url":"https://github.com/skx.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# s-lang\n\nThis repository contains a compiler for a minimal programming language, targeting linux/amd64 systems.\n\nThe generated code contains no external dependencies, so when compiled they are static binaries and do not depend upon libC, etc.   The standard library routines which are not used may be removed by the linker, reducing size, and generated binaries start around 1k.\n\n* Written in Golang for portability, although the generated code is obviously Linux/AMD64-specific.\n* We have a real lexer, and parser, and internally generate an AST which is then walked to generate the assembly language representation of the program.\n* We can automatically invoke the external `as` and `ld` binaries to compile and link if desired.\n\nIn terms of features:\n\n* Single-pass compiler which generates an assembly output for programs.\n* Parsing uses recursive descent with precedence layers:\n  * Maths operations: `+`, `-`, `*`, `/`, `%`, and `^`.\n  * Comparison operations: `\u003c`, `\u003c=`, `==`, `!=`, `\u003e`, `\u003e=`, for integers, floats, and mixtures of the two.\n  * Logical operations: `\u0026\u0026` and `||`.\n  * Decrement/Increment support for variables ( `i++;`, or `index--;` for example).\n* Support for integers, floats, and strings.\n  * String literals are interned.\n  * So you can call \"`print(\"Steve\");`\" 100 times and still see the text \"Steve\" in the binary only once.\n* The ability to include inline assembly via `inline { .. }`.\n  * `inline` statements are generated inline as they are encountered.\n  * If you want to add new sections then use a `data { ..  }`-block, that is guaranteed to be inserted at the end of the assembly-generation.  So you can add \"`.section blah .. ..`\" without fear of breaking things.\n* Looping is available with the `while` statement, including standard support for `break` and `continue`.\n* Conditional support with `if` with `else` branch too.\n* We support `switch` statements, albeit only with integer/character literals for the `case` matches.\n  * `default` is supported too, of course.\n* Some support for catching signals via \"magic functions\":\n  * `sigint()` is called, if defined, when SIGINT is received (i.e. Ctrl-C is pressed).\n    * We use this in [examples/life.in](examples/life.in) to clear the screen, and restore the cursor.\n  * `sigfpe()` is called, if defined, when SIGFPE is received.\n    * This is the floating-point exception generated upon division by zero.\n* Cleanup via `at_exit()` which is called, if defined, when the program terminates, either due to signals being caught, or at an ordinary exit.\n* User defined functions, with default values.\n\nAnti-features, or limitations:\n\n* The language is built around numbers (integers\u0026floats), and strings.\n  * We have no support for arrays, hashes, or structures.\n  * That said you can _fake_ arrays via indexing into characters of strings, or `malloc`'d areas of memory.\n    * You can see that done in this [test/jumptable.in](test/jumptable.in) where we use it to implement a simple dynamic dispatch routine.\n* There are only a few functions in the standard library.\n\nThat said the code is clean, and hopefully readable, and we've got good test-case coverage of both the  golang packages, and the functional operation).\n\n\n\n## Example Programs\n\nSee [examples/](examples/) for _real_ programs.  A couple of highlights:\n\n* [examples/brainfuck.in](examples/brainfuck.in) - Brainfuck interpreter.\n  * Contains three hardcoded programs inline:\n    * The classic \"Hello World\" program.\n    * A simple \"cat\", which copies STDIN to STDOUT.\n    * The impressive mandelbrot generation program!\n  * If executed with the path to a file containing a brainfuck program it will read and execute that.\n* [examples/life.in](examples/life.in)\n  * Conway's Game of Life.\n  * Randomly populate 20% of the arena, and evolve until bored!\n* Math examples:\n  * [examples/factorial.in](examples/factorial.in) - Calculate factorials 1-20.\n  * [examples/fibonacci.in](examples/fibonacci.in) - Calculate fibonacci sequence, using recursion.\n  * [examples/fizzbuzz.in](examples/fizzbuzz.in) - Calculate fizzbuzz 0-100.\n  * [examples/primes.in](examples/primes.in) - Calculate first 100 prime numbers.\n  * [examples/num2hex.in](examples/num2hex.in) - Convert a decimal number to a hex string.\n\nSyntax is covered pretty well in our \"misc example\" file:\n\n* [examples/example.in](examples/example.in) - Misc. Examples.\n\n\n\n## Syntax\n\nThe following is a tour of our language:\n\n    # Comments are prefixed with \"#\" and last until the end of the line.\n\n    # Set a variable and print it.\n    let a = 3;\n    print( a );\n\n    # Indexing works\n    let a = \"Steve\";\n    print(a[0],\"\\n\");\n\n    # Updating too\n    a[1] = 42;\n    print(a,\"\\n\");\n\n    # Printing a newline is common.\n    newline();\n\n    # simple loops with \"while\"\n    let x = 10;\n\n    # Looping on a variable is the same as \"while ( x \u003e 0 ) ..\"\n    while(x) {\n       print(\"The value in my loop is \", x, \"\\n\");\n       x--;\n    }\n\n    inline {\n       # Inline assembly here\n    }\n\n    # Conditional expressions are present\n    if (x \u003e= 3) {\n      print(\"x \u003e= 3\\n\");\n    } else {\n      print(\"x is not \u003e= 3\\n\");\n    }\n\n    # Printing of integer and string literal works too.\n    print( \"steve\", \" \", 21);\n\n    # Exit with the given status\n    exit(1 + 2 * 3);\n\nTrailing semicolons are mandatory (because that simplifies the parser. Sorry!)\n\nThere is an emacs lisp mode, [s-lang.el](s-lang.el) providing syntax highlighting for our language, although there are no other features beyond that.\n\n\n\n## Usage\n\nOnce built (and optionally installed) the `s-lang` binary may be used to\ngenerate, compile, or inspect the output of various stages via a number of\nsub-commands.\n\nHere we see the available sub-commands that you might choose to use, though in\npractice only the last three are useful for users.\n\n\n### lex\n\nThis is an internal command to show what the lexer makes of a given input file:\n\n     s-lang lex examples/example.in\n\n\n### parse\n\nThis is an internal command to show what the parser makes of a given input file:\n\n     s-lang parse examples/example.in\n\n\n### generate\n\nThis is one of the main commands, and generates an assembly language version of the input file:\n\n     s-lang generate [-output out.s] examples/example.in\n\nYou could assemble that output, and link it, like so:\n\n     as -msyntax=intel -mnaked-reg out.s -o out.o\n     ld -s -o out out.o\n\nConfirm the assembly is sane:\n\n     objdump -M intel -S out\n\nThen run it:\n\n    ./out\n\nThe `compile` sub-command automates the process of generating source, compiling it, and linking it to produce a final binary.\n\n\n### compile\n\nThis performs the same generation as in the `generate` sub-command, but also runs the assembler and linker for you:\n\n     s-lang compile [-output a.out] examples/example.in\n\nTypically you'd run something like this to generate and execute in one go:\n\n     s-lang compile examples/example.in \u0026\u0026 ./a.out\n\n(Or use the `execute` sub-command to create a binary and run it in one step.)\n\n\n### execute\n\nThis performs the same generation as in the `compile` sub-command, but also runs the resulting binary for you:\n\n     s-lang execute [-output a.out] examples/example.in\n\n\n\n## STDLIB\n\nWe embed a small number of functions within the generated programs, our so-called \"standard library\".  These are functions which seemed to be useful enough to include globally, and each function that accepts arguments has type-checking, both at compile-time and run-time.\n\n* `argc()`\n  * Return the count of supplied command-line arguments.\n* `argv(N)`\n  * Return the Nth command-line argument, as a string.\n* `exit(N)`\n  * Terminate execution with the given exit-code.\n* `filesize(STR)`\n  * Return the size of the given file.\n* `float2int(F)`\n  * Convert the given floating-point number to an integer.\n* `getc()`\n  * Read a single character from STDIN, returns 0 on EOF.\n* `getenv(STR)`\n  * Return the contents of the environmental variable with the given name.\n* `int2float(N)`\n  * Convert the given integer to a floating point.\n* `malloc(N)`\n  * Allocate N bytes on the heap.\n  * **NOTE**: We have no corresponding `free`.\n* `newline`\n  * Print a newline to STDOUT.\n* `panic(STR)`\n  * Print the given message, and exit.\n  * Do not invoke `at_exit()`.\n* `print(...,...,...)`\n  * This function is variadic; it will accept any number of arguments of any type.\n  * Print each argument in turn.\n* `putc(N)`\n  * Print the ASCII character corresponding to the given integer to STDOUT, i.e `putc(42);` will print `*`.\n* `rand(N)`\n  * Return a random number between 0-(N-1).\n* `readfile(STR)`\n  * Return the contents of the given file.\n* `sleep(N|F)`\n  * Sleep for the given duration, integer or float.\n* `sqrt(N|F)`\n  * Calculate the square root of the given integer/float.\n  * Always returns a floating-point result.\n* `strcat(STR, STR)`\n  * Combine the two strings, and return the new string.\n* `strcmp(STR, STR)`\n  * Compare two strings for equality, return `0` if equal.\n* `strdup(STR)`\n  * Allocate a copy of the given string, and return it.\n* `strlen(STR)`\n  * Return the length of the given string.\n* `str2int(STR)`\n  * Convert a string into an integer.\n* `str2float(STR)`\n  * Convert a string into a floating-point number.\n\nYou can find the implementation of our standard library routines beneath the [compiler/templates/stdlib](compiler/templates/stdlib) directory.\n\n\n### Adding to the standard library\n\nIf you wish to add a new function which will be available to all compiled programs you need to add it to a new file beneath `compiler/templates/stdlib`, then rebuild the compiler.\n\nFor example if you want to define the new function `foo`:\n\n* Create `compiler/templates/stdlib/foo.tmpl`\n* Inside there define a new function, with the label `foo:`.\n* Rebuild the compiler (with \"`go build .`\")\n\nOnce that is done your prgrams can immmediately call it:\n\n    let a = foo();\n\nThis works because internally a call to `foo( [args] )` is converted into a call to the assembly-language function named `foo`.  (i.e. Defined with the label `foo:`).  You can use `inline` to define/call such a function manually if you wish, providing you follow our function ABI.\n\nIt is assumed your function will check the types of any arguments it receives, but you can add an entry to the type-checking package, described later, if you wish to add some additional compile-time type checking.\n\n\n\n## Type Checking\n\nThere are two forms of possible type-checking:\n\n* Type checking at compilation time.\n* Type checking at run time.\n\nAt compilation time we can detect invalid argument counts for standard library functions _and_ user-defined functions.  For example this is caught:\n\n     function foo( x ) {  print( \"I got: \", x , \"\\n\" );\n     foo();  # ERROR - Expected one argument, received zero.\n\nFor checking actual types at compile time we're limited, we can detect this error:\n\n     strlen(3);  # Wrong type, expected string but got int\n\nHowever this is permitted:\n\n     let a = 3;\n     print(strlen(a))  # Type information didn't survive the assignment\n\n**NOTE**: Compile-time type checking of standard-library functions requires an explicit definition within our [check/](check) package.  If you add a new function please do add an entry there.\n\nRun-time checking of types is deferred to our standard library routines, and they _should_ all check their argument types are valid before they execute their jobs.  They will return an error string \"strlen: expected STRING\", or similar, instead of their normal result.\n\n\n\n## Types\n\nAs noted we support three different variable types (integer, float, and string/pointer).  We use the lower two bits of values to store their types:\n\n* integers have their lower two bits set to `00`\n* pointers have their lower two bits set to `01`.\n* floats are allocated on the heap, and the pointer has the lower two bits set to `10`.\n\nThere is space left for one more type, if the lower two bits are `11`, in the future.\n\n\n### Type Examples\n\nYou should be able to work this out from the \"Types\" section above, or from looking at the code, but here are examples of getting values for each of our types:\n\nGetting an integer from RAX:\n\n        sar rax, 2  # Shift right, removing lower two bits.\n\nGetting the float from RAX:\n\n        and rax, -4       # Clear the type bits\n        movsd xmm0, [rax] # Load the heap-allocated float.\n\nGetting a string/pointer from RAX:\n\n        mov rdi, rax  # Get the string\n        and rdi, -4   # Remove typing bits\n\nReturning a number from a function:\n\n        mov rax, 42   # Load the value\n        sal rax, 2    # Lower two bits are now 00\n\nReturning a float from a function:\n\n        call alloc8         # allocate 8-byte boxed float\n        movsd [rax], xmm0   # store payload\n        or rax, 2           # tag pointer as float (10)\n\nReturning a string from a function:\n\n        mov rax, offset str_ptr  # Load the string\n        or rax, 1                # Mark the type\n\nFinally here's how to do type-checking of the parameter in RAX:\n\n        mov rcx, rax\n        and rcx, 3\n\n        cmp rcx, 0\n        je print_integer\n\n        cmp rcx, 1\n        je print_string\n\n        cmp rcx, 2\n        je print_float\n\n        cmp rcx, 3\n        je print_reserved\n        ret\n\nYou can see some tips on debugging with `gdb` in [DEBUGGING.md](DEBUGGING.md).\n\n**NOTE**  Our `alloc8` and `malloc` functions will do their own error-checking, if allocation fails they will print a message and terminate execution.  That means there is no need to check the result of calls to allocation routines.\n\n\n\n## ABI\n\nWe define a simple ABI for function invocation:\n\n* All function parameters are passed on the stack.\n* The _number_ of parameters is passed in the RAX register.\n\nYou can see how this is handled in [our standard-library functions](compiler/templates/stdlib) for reference, but do remember that variables have types.   As an example if you want to print the integer 17  using inline assembly you would run:\n\n     inline {\n        mov rax, 17  # store payload\n        sal rax, 2   # Bottom two bits should be \"00\".\n        push rax     # parameters are passed on the stack\n\n        mov rax, 1   # one argument is being passed\n        call print   # call the stdlib function\n     }\n\n\n\n## Optimizing Generated Binary Size\n\nWhen the `compile` sub-command is executed we run generate `NAME.s` from `NAME.in`, and then:\n\n      as -msyntax=intel -mnaked-reg NAME.s -o NAME.o\n      ld --gc-sections -s -no-pie -z noseparate-code -o NAME NAME.o\n\nThe linker command here shrinks our binaries significantly, if you don't want/need that size-saving then a more typical usage will suffice:\n\n      ld -o NAME NAME.o\n\nThe difference in the two approaches can be seen by our brainfuck example:\n\n* Just using `ld -o brainfuck brainfuck.o`:\n  * `-rwxr-xr-x 1 skx skx 38648 May 24 13:27 brainfuck`\n* With our longer `ld` usage:\n  * `-rwxr-xr-x 1 skx skx 18208 May 24 13:28 brainfuck`\n\nWe went from 25k to 18k, which is a good saving.\n\nYou can see some tips on debugging with `gdb` in [DEBUGGING.md](DEBUGGING.md), if you want to ease your debugging it is recommended you assemble and link yourself, that way there will be debug information available to you.\n\n\n\n## Development \u0026 Testing\n\nDevelopment is nearing completion now.  There are a few small and obvious things to add, but at the same time the scripting language itself is pretty complete, the standard library is complex enough to write real programs, and I suspect my urge to add new things will diminish over time.\n\nUpdates _should_ be contributed by pull-requests which address open issues, but sometimes I'm less strict with myself than I should be.\n\nI've written test-cases covering most of the implementation, which you can run in the standard manner:\n\n```\n$ go test ./...\nok      s-lang\t0.005s\nok      s-lang/compiler\t0.009s\nok      s-lang/lexer\t0.006s\nok      s-lang/parser\t0.003s\n```\n\nThere is also support for the fuzz-testing that golang provides, you can run five minutes of fuzz-testing by executing the following (remove the `-fuzztime=300s` to run _forever_, and remove `-parallel=1` to run more than a single instance at a time):\n\n```\ngo test -fuzztime=300s -parallel=1 -fuzz=FuzzProject -v\n```\n\nIn addition to the golang tests, and fuzzer, we have some functional test-cases beneath `test/`, there is a trivial driver which executes each of the sample programs, and compares the output produced to known-good results:\n\n```\n$ cd test \u0026\u0026 make\nexpr.in\n compiling expr.in to expr\n executing expr \u003e expr.out\n comparing expr.out to expr.expected\n cleanup\n...\n```\n\nRunning `make test` should run both of those things.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fskx%2Fs-lang","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fskx%2Fs-lang","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fskx%2Fs-lang/lists"}