Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
version: 2
updates:
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
49 changes: 47 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
Sprout is a small programming language that runs on Go.
It ships with a lexer, a Pratt parser, and two execution engines.
Version 0.2 adds a stack-based bytecode virtual machine.
Version 0.3 adds a module system for splitting programs across files.
The interpreter and the VM share one runtime and one standard library.
Sprout produces friendly diagnostics that point at the exact problem.

Expand All @@ -16,6 +17,7 @@ The codebase is structured to grow cleanly over time.
- A bytecode compiler and a stack-based virtual machine.
- A tree-walking interpreter that shares the runtime with the VM.
- A disassembler for the compiled instruction stream.
- A module system with imports, exports, and cycle detection.
- Source diagnostics with a gutter, line, and caret.
- A small standard library for real example programs.
- A REPL for interactive experiments.
Expand Down Expand Up @@ -82,7 +84,7 @@ Run the tour in `docs/tour.md` for a full walkthrough.
| `sprout repl` | Starts a session. |
| `sprout lex file.spr` | Shows the tokens. |
| `sprout parse file.spr` | Shows the syntax tree. |
| `sprout check file.spr` | Checks without running. |
| `sprout check file.spr` | Checks the file and its imports without running. |
| `sprout version` | Shows the version. |

Pass a file path with no command to run it.
Expand Down Expand Up @@ -148,7 +150,6 @@ Version 0.2 adds a compiler and a stack-based virtual machine.
The compiler turns a syntax tree into bytecode.
The VM executes that bytecode with an operand stack and call frames.
Both engines share the runtime, so they behave identically.

The `dis` command shows the compiled instructions.

```text
Expand All @@ -169,6 +170,41 @@ Each line shows the offset, the opcode, and the source position.
The engine parity tests prove the VM matches the interpreter.
See `docs/bytecode.md` for the full reference.

## Modules

Version 0.3 adds a module system.
Split a program across files with `import` and `export`.
A module runs once, in its own scope.
Only exported names are visible to importers.

Save a library module.

```sprout
// lib/mathx.spr
export fn double(x) {
return x * 2
}
```

Use it from a program.

```sprout
import "lib/mathx"

print(mathx["double"](21)) // 42
```

The bound name comes from the file name.
Use `as` to choose a different name.
Paths resolve against the importing file directory.
Circular imports are an error.
The loader caches each module, so it runs once per program.

The `check` command validates the whole import graph.
The VM compiles imports into `IMPORT` instructions.
Run `examples/modules.spr` to see a working library.
See `docs/modules.md` for the full reference.

## Standard library

The standard library is small and documented.
Expand Down Expand Up @@ -197,6 +233,7 @@ The `examples` directory holds documented programs.
- `math.spr` shows numbers and rounding.
- `higher_order.spr` uses map, filter, and fold.
- `guess.spr` is an interactive game.
- `modules.spr` imports the library in `examples/lib`.

Each example has a golden output in `test/golden`.
Both engines must match the goldens.
Expand All @@ -213,6 +250,7 @@ internal/lexer the scanner
internal/ast the syntax tree
internal/parser the Pratt parser
internal/checker static analysis
internal/module module loading, caching, and cycles
internal/runtime value semantics and the standard library
internal/interp the tree-walking interpreter
internal/code opcodes and the instruction stream
Expand All @@ -225,6 +263,7 @@ internal/repl the interactive session
Each stage is independent.
The parser feeds the checker and the compiler.
The runtime is the single source of truth for both engines.
The module loader runs on either engine.
Adding a feature means updating the runtime, then both engines stay in step.

## Development
Expand Down Expand Up @@ -262,6 +301,7 @@ All tests pass on Go 1.22 and newer.
| `internal/lexer` | Tokens, positions, and lexer errors. |
| `internal/parser` | Precedence, statements, and recovery. |
| `internal/checker` | Scope and static errors. |
| `internal/module` | Path resolution, caching, and cycles. |
| `internal/runtime` | Arithmetic, comparison, and indexing. |
| `internal/interp` | Evaluation, closures, and runtime errors. |
| `internal/code` | Opcodes, the builder, and disassembly. |
Expand All @@ -280,6 +320,9 @@ Version 0.2 is complete. It adds the bytecode virtual machine.
It keeps the same parser and checker.

Version 0.3 adds a module system and a build tool.
The module system is complete. Imports, exports, and cycle detection ship
in this release. The build tool remains.

Version 0.4 adds structs, methods, and interfaces.
Version 0.5 adds result types and pattern matching.
Version 0.6 adds concurrency with channels.
Expand All @@ -294,6 +337,8 @@ Version 0.6 adds concurrency with channels.
- The standard library is small by design.
- The VM materializes a loop iterable before the loop starts.
- `break` and `continue` inside a closure are not supported.
- Module values are read-only. There is no package index yet.
- Import errors point at the import statement, not inside the module.

## License

Expand Down
9 changes: 8 additions & 1 deletion cmd/sprout/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,15 @@ import (
"github.com/sprout-lang/sprout/internal/diag"
"github.com/sprout-lang/sprout/internal/interp"
"github.com/sprout-lang/sprout/internal/lexer"
"github.com/sprout-lang/sprout/internal/module"
"github.com/sprout-lang/sprout/internal/parser"
"github.com/sprout-lang/sprout/internal/repl"
"github.com/sprout-lang/sprout/internal/source"
"github.com/sprout-lang/sprout/internal/token"
"github.com/sprout-lang/sprout/internal/vm"
)

const version = "0.2.0"
const version = "0.3.0"

func main() {
os.Exit(run(os.Args[1:]))
Expand Down Expand Up @@ -299,6 +300,12 @@ func runCheck(args []string) int {
return 1
}
}
// Check every module the program imports, without running them.
loader := module.New(nil)
if err := loader.CheckProgram(file, prog); err != nil {
fmt.Fprintf(os.Stderr, "sprout: %v\n", err)
return 1
}
fmt.Println("ok")
return 0
}
Expand Down
18 changes: 18 additions & 0 deletions docs/bytecode.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,8 @@ This matches the interpreter, so closures see their own copy.
| BUILD_MAP | count | Build a map. |
| MAKE_ITER | none | Pop an iterable, push an iterator. |
| ITER_NEXT | target | Advance, jump when done. |
| IMPORT | index | Load a module and push it. |
| EXPORT | index | Record the top value as an export. |
| NEG | none | Negate the top. |
| NOT | none | Invert the truthiness. |
| ADD | none | Add the top two values. |
Expand Down Expand Up @@ -138,6 +140,22 @@ The runtime covers arithmetic, comparison, indexing, and iteration.
It also owns the standard library.
A builtin is a runtime value with a name and a Go function.

## Modules

The compiler turns an `import` statement into an `IMPORT` instruction.
The instruction carries the module path from the constant pool.
The VM loads the module through the shared module loader and pushes it.

The `export` keyword compiles the wrapped declaration, then reads the
declared name and records it with an `EXPORT` instruction.
A program that runs as a module fills an export table with these names.
A program that runs directly ignores the table.

The VM runs every imported module on a fresh VM.
All modules share one loader, so caching and cycle detection span the
whole import graph.
See `docs/modules.md` for the language-level rules.

## Errors

The VM reports errors with a source position and a call stack.
Expand Down
19 changes: 18 additions & 1 deletion docs/grammar.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Sprout Grammar

This document is the formal grammar of Sprout version 0.2.
This document is the formal grammar of Sprout version 0.3.

## Notation

Expand Down Expand Up @@ -35,13 +35,18 @@ identifier := letter {letter | digit | "_"}
Numbers do not start or end with an underscore.
A floating-point literal needs a digit before the decimal point.

The keywords are `let`, `const`, `fn`, `if`, `elif`, `else`, `while`,
`for`, `in`, `return`, `break`, `continue`, `true`, `false`, `nil`,
`and`, `or`, `not`, `import`, `export`, and `as`.

## Program structure

```
program := statement*
statement := let_decl | const_decl | fn_decl
| if_stmt | while_stmt | for_stmt
| return_stmt | break_stmt | continue_stmt
| import_stmt | export_stmt
| expr_stmt
```

Expand All @@ -62,6 +67,18 @@ The `fn` keyword also creates an anonymous function as an expression.
fn_expr := "fn" "(" params ")" block
```

## Modules

```
import_stmt := "import" string ["as" identifier]
export_stmt := "export" (let_decl | const_decl | fn_decl)
```

An import binds a module value to a name.
The name comes from the `as` clause or from the path base.
Imports and exports only appear at the top level of a file.
See `docs/modules.md` for the full module reference.

## Control flow

```
Expand Down
135 changes: 135 additions & 0 deletions docs/modules.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
# Modules

This page describes the Sprout module system.
It covers imports, exports, and how the loader resolves files.
Modules let you split a program into reusable files.

## Overview

A module is a Sprout source file.
The `import` statement loads a module and binds it to a name.
The `export` keyword marks declarations as visible to importers.

A module runs once, in an isolated scope.
Its exported names form a read-only namespace.
Other files cannot see private names.

## Imports

The `import` statement takes a module path.

```sprout
import "lib/numbers"
```

The bound name comes from the file name.
The path `lib/numbers` binds the name `numbers`.
A missing `.spr` extension is added when needed.

Use `as` to choose a different name.

```sprout
import "lib/strings" as text
```

The path is relative to the file that contains the import.

## Exports

The `export` keyword marks a top-level declaration as public.

```sprout
export let answer = 42
export const pi = 3.14
export fn square(x) {
return x * x
}
```

Only `let`, `const`, and `fn` declarations can be exported.
Imports and exports only appear at the top level of a file.
Private names still work inside the module.

## Using a module

An imported module is a value of type `module`.
Reach an export with index access, then call it like any function.

```sprout
import "lib/numbers"

print(numbers["square"](5)) // 25
print(numbers["answer"]) // 42
```

Exported functions share the module scope.
They can call the module private helpers.
They can read and write the module state.

## Example

Save two files in a `lib` directory.

```sprout
// lib/mathx.spr
export fn double(x) {
return x * 2
}
```

```sprout
// main.spr
import "lib/mathx"

print(mathx["double"](21)) // 42
```

Run the main file.

```text
sprout run main.spr
```

## Loading rules

The loader follows a few simple rules.

- A module runs once per program.
Later imports return the same value.
- Paths resolve against the importing file directory.
- Circular imports are an error.
- A missing or broken module is an error.
- The loader uses the same engine that runs the program.

These rules keep module behavior deterministic.
Both the interpreter and the VM follow them.

## Checking

The `sprout check` command validates the whole import graph.
It loads every imported module and checks it without running it.
A broken or missing module fails the check.

```text
sprout check main.spr
```

## Errors

Import errors point at the import statement.
The message names the module and the cause.

```text
error: circular import of "b.spr"
```

A parse or check error in a module reports the module path and line.
Run `sprout check` to see the full picture before running.

## Limitations

- A module value is read-only. Writes to it are an error.
- Import paths cannot reach outside the file system.
- A module with a circular dependency is rejected.
- Module state is shared by reference. Values like lists are not copied.
- There is no package index yet. Imports use file paths only.
4 changes: 3 additions & 1 deletion docs/stdlib.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Standard Library

This page lists the built-in functions of Sprout 0.2.
This page lists the built-in functions of Sprout 0.3.
They are available in every program without an import.

## Output
Expand All @@ -22,6 +22,8 @@ They are available in every program without an import.
| `type(x)` | Returns the type name of x as a string. |

`int` and `float` accept numbers, booleans, and numeric strings.
`type` returns one of `int`, `float`, `string`, `bool`, `nil`, `list`,
`map`, `function`, `range`, or `module`.

## Values

Expand Down
Loading