@algosail/lang 0.2.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -0
- package/cli/typecheck.js +39 -0
- package/docs/ARCHITECTURE.md +50 -0
- package/docs/CHANGELOG.md +18 -0
- package/docs/FFI-GUIDE.md +65 -0
- package/docs/RELEASE.md +36 -0
- package/docs/TESTING.md +86 -0
- package/example/tictactoe.sail +0 -0
- package/index.js +2 -0
- package/package.json +21 -0
- package/test/integration.test.js +61 -0
package/README.md
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# @algosail/lang
|
|
2
|
+
|
|
3
|
+
Sail language tooling. Re-exports parser and typecheck.
|
|
4
|
+
|
|
5
|
+
## Exports
|
|
6
|
+
|
|
7
|
+
```javascript
|
|
8
|
+
import { createParser, typecheck } from '@algosail/lang'
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Docs
|
|
12
|
+
|
|
13
|
+
- [FFI-GUIDE.md](docs/FFI-GUIDE.md) — JSDoc format for FFI modules
|
|
14
|
+
- [ARCHITECTURE.md](docs/ARCHITECTURE.md) — Pipeline, packages, dependencies
|
|
15
|
+
- [TESTING.md](docs/TESTING.md) — Brittle setup, integration tests
|
|
16
|
+
- [CHANGELOG.md](docs/CHANGELOG.md) — Version history
|
|
17
|
+
- [RELEASE.md](docs/RELEASE.md) — Release checklist
|
|
18
|
+
|
|
19
|
+
## Packages
|
|
20
|
+
|
|
21
|
+
| Package | Description |
|
|
22
|
+
|---------|-------------|
|
|
23
|
+
| @algosail/parser | Parse Sail + JS → symbol table |
|
|
24
|
+
| @algosail/typecheck | Type checking |
|
|
25
|
+
| @algosail/compiler | Sail → JS (CLI: `sail-compile`) |
|
|
26
|
+
| @algosail/builtins | Builtin words |
|
|
27
|
+
| @algosail/tree-sitter | Grammar |
|
|
28
|
+
| @algosail/lsp | LSP server (hover, completion, go-to-definition) |
|
package/cli/typecheck.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createParser } from '@algosail/parser'
|
|
3
|
+
import { typecheck } from '@algosail/typecheck'
|
|
4
|
+
import { readFile } from 'node:fs/promises'
|
|
5
|
+
import { pathToFileURL } from 'node:url'
|
|
6
|
+
import { resolve } from 'node:path'
|
|
7
|
+
|
|
8
|
+
const filePath = process.argv[2]
|
|
9
|
+
|
|
10
|
+
if (!filePath) {
|
|
11
|
+
console.error('Usage: sail-typecheck <path-to-file.sail>')
|
|
12
|
+
process.exit(1)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const absPath = resolve(process.cwd(), filePath)
|
|
16
|
+
const uri = pathToFileURL(absPath).href
|
|
17
|
+
|
|
18
|
+
let text
|
|
19
|
+
try {
|
|
20
|
+
text = await readFile(absPath, 'utf8')
|
|
21
|
+
} catch (err) {
|
|
22
|
+
console.error(`sail-typecheck: cannot read ${filePath}: ${err.message}`)
|
|
23
|
+
process.exit(1)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const parser = await createParser()
|
|
27
|
+
const result = await parser.parseSail(uri, text)
|
|
28
|
+
const parseErrors = result.errors ?? []
|
|
29
|
+
const typeErrors = typecheck(result)
|
|
30
|
+
const errors = [...parseErrors.map((e) => ({ ...e, message: e.type === 'error' ? `Parse error: unexpected ${JSON.stringify(e.text)}` : `Parse error: missing ${JSON.stringify(e.text)}` })), ...typeErrors]
|
|
31
|
+
|
|
32
|
+
if (errors.length > 0) {
|
|
33
|
+
for (const err of errors) {
|
|
34
|
+
const { row, column } = err.startPosition ?? {}
|
|
35
|
+
const loc = row != null ? `:${row + 1}:${(column ?? 0) + 1}` : ''
|
|
36
|
+
console.error(`${filePath}${loc}: ${err.message}`)
|
|
37
|
+
}
|
|
38
|
+
process.exit(1)
|
|
39
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Sail Architecture
|
|
2
|
+
|
|
3
|
+
## Pipeline
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
Sail source (.sail) → [Parser] → Symbol Table → [Typecheck] → [Compiler] → JavaScript
|
|
7
|
+
↑ ↑ ↑
|
|
8
|
+
FFI (.js) → [parseJsDoc] ─┘ └── modules, groups, tags, words
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
1. **Parser** (`@algosail/parser`): Builds symbol table from Sail and JS sources
|
|
12
|
+
2. **Typecheck** (`@algosail/typecheck`): Validates words and tags against signatures
|
|
13
|
+
3. **Compiler** (`@algosail/compiler`): Emits JS from validated symbol table
|
|
14
|
+
|
|
15
|
+
## Packages
|
|
16
|
+
|
|
17
|
+
| Package | Role |
|
|
18
|
+
|---------|------|
|
|
19
|
+
| `@algosail/tree-sitter` | Grammar (grammar.js), WASM lexer/parser for Sail |
|
|
20
|
+
| `@algosail/parser` | buildSymbolTable, parseJsDoc, Sail + JS → AST |
|
|
21
|
+
| `@algosail/builtins` | Builtin words (DUP, SWAP, DIP, MATCH, …) with sigs |
|
|
22
|
+
| `@algosail/shared` | getStepSignature, getStepStackEffect (used by typecheck, compiler) |
|
|
23
|
+
| `@algosail/typecheck` | checkWord, checkTag, stack-effect validation |
|
|
24
|
+
| `@algosail/compiler` | compile(), emitWord, emitStep → JS |
|
|
25
|
+
| `@algosail/lsp` | LSP server (validate, completion, hover, go-to-definition) |
|
|
26
|
+
| `@algosail/lang` | Re-exports typecheck, createParser; integration tests |
|
|
27
|
+
|
|
28
|
+
## Data Flow
|
|
29
|
+
|
|
30
|
+
- **Symbol table**: `{ modules, groups, tags, words, errors, imports }`
|
|
31
|
+
- **Word**: `{ name, sig, signature, body, doc }` — body is steps (builtin_word, word_ref, quotation, tag, …)
|
|
32
|
+
- **Typecheck** uses `builtins` for builtin signatures; FFI words come from `parseJsDoc` (modules)
|
|
33
|
+
- **Compiler** uses builtins for emit logic; FFI calls go through `emitStep` (CALL to imported module)
|
|
34
|
+
|
|
35
|
+
## Dependencies
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
tree-sitter (grammar)
|
|
39
|
+
↓
|
|
40
|
+
parser (tree-sitter, tree-sitter-javascript, web-tree-sitter)
|
|
41
|
+
↓
|
|
42
|
+
builtins (no deps)
|
|
43
|
+
↓
|
|
44
|
+
typecheck (builtins)
|
|
45
|
+
compiler (parser, typecheck, builtins)
|
|
46
|
+
lsp (parser, typecheck, builtins)
|
|
47
|
+
lang (parser, typecheck, compiler)
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
**Note:** `tree-sitter` uses regex `/[A-Z][A-Z0-9_]*/` for `builtin_word` — decoupled from builtins (Phase 2.2 done).
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [Unreleased]
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- Integration tests (parse → typecheck → compile → run) in `lang/test/integration.test.js`
|
|
7
|
+
- `npm test` in @algosail/lang
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
- Documentation: compiler README (source maps, error format, CLI options)
|
|
11
|
+
- Documentation: LSP README (hover, definition)
|
|
12
|
+
- Documentation: TESTING.md (integration tests section)
|
|
13
|
+
|
|
14
|
+
## [0.2.8] - previous
|
|
15
|
+
- @algosail/lang: sail-typecheck CLI, re-exports typecheck + createParser
|
|
16
|
+
- @algosail/compiler: source maps, JSDoc, async/await, error hints
|
|
17
|
+
- @algosail/lsp: hover, go-to-definition
|
|
18
|
+
- @algosail/typecheck: effects (addEffect, removeEffect)
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Sail FFI: JSDoc Format
|
|
2
|
+
|
|
3
|
+
Sail reads type information from JavaScript/JS modules via JSDoc. This document describes the format.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
FFI modules are JS files with `/** ... @sail ... */` blocks. The parser (`@algosail/parser`) uses `parseJsDoc` to extract groups, tags, and words. Compiled Sail output must emit the same format so it can be used as FFI by other Sail code and JS consumers.
|
|
8
|
+
|
|
9
|
+
## Word (exported function)
|
|
10
|
+
|
|
11
|
+
A JSDoc block **immediately before** an `export function` declaration defines a Sail word. The function name becomes the word name when called as `~Module/wordName`.
|
|
12
|
+
|
|
13
|
+
```javascript
|
|
14
|
+
/**
|
|
15
|
+
* @sail
|
|
16
|
+
* @of ( * -- Int ) ( Convert a raw value to an integer )
|
|
17
|
+
*/
|
|
18
|
+
export function of(a) {
|
|
19
|
+
return parseInt(a, 10)
|
|
20
|
+
}
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
**Format after `@sail`:**
|
|
24
|
+
- `@functionName ( stack_sig ) ( description )`
|
|
25
|
+
- `stack_sig`: `inputs -- outputs`, e.g. `( * -- Int )`, `( a b -- b a )`
|
|
26
|
+
- `*` = raw value (untyped literal)
|
|
27
|
+
- Types: `Int`, `Str`, `Bool`, `[a]` (list), `( a -- b )` (quotation), `&Group{Int}` (tagged union)
|
|
28
|
+
|
|
29
|
+
**Full word form** (when the block starts with `@wordName`):
|
|
30
|
+
```
|
|
31
|
+
@wordName ( sig ) ( doc )
|
|
32
|
+
```
|
|
33
|
+
The sail parser treats everything after `@sail` as Sail source and parses it with the Sail grammar (word_def, sig, etc.).
|
|
34
|
+
|
|
35
|
+
## Groups and Tags
|
|
36
|
+
|
|
37
|
+
Standalone JSDoc blocks (not before export) can define groups and tags:
|
|
38
|
+
|
|
39
|
+
```javascript
|
|
40
|
+
/**
|
|
41
|
+
* @sail
|
|
42
|
+
* &Maybe a
|
|
43
|
+
* #Just a
|
|
44
|
+
* #Nothing
|
|
45
|
+
*/
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Parsed as Sail: `&Maybe a` (group with type param), `#Just a` and `#Nothing` (tags).
|
|
49
|
+
|
|
50
|
+
## Extraction Rules
|
|
51
|
+
|
|
52
|
+
- Comment must start with `/**` and contain `@sail`
|
|
53
|
+
- Content after `@sail` is parsed as Sail source (groups, tags, words)
|
|
54
|
+
- For export pairs: `comment` + `export function name` → word `name` with body from JSDoc
|
|
55
|
+
- If sailText starts with `@wordName` (e.g. `@of`), it's used as-is; else prefixed with `@functionName`
|
|
56
|
+
|
|
57
|
+
## References
|
|
58
|
+
|
|
59
|
+
- Parser: `parser/lib/jsDoc.js` — `parseJsDoc`, `findExportSailPairs`, `extractSailBlock`
|
|
60
|
+
- Examples: `zed/test/int.js`, `zed/test/buffer.js`
|
|
61
|
+
- Builtins use a different format (JS objects with `sig`, `signature`, `docs`) — see `builtins/index.js`
|
|
62
|
+
|
|
63
|
+
## Buffer (Bare/Node)
|
|
64
|
+
|
|
65
|
+
Для Bare + Hyperswarm + RocksDB: `zed/test/buffer.js` — FFI-модуль с `from`, `alloc`, `concat`.
|
package/docs/RELEASE.md
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Release Checklist
|
|
2
|
+
|
|
3
|
+
## Pre-release
|
|
4
|
+
|
|
5
|
+
1. Run all tests:
|
|
6
|
+
```bash
|
|
7
|
+
cd parser && npm test
|
|
8
|
+
cd ../typecheck && npm test
|
|
9
|
+
cd ../compiler && npm test
|
|
10
|
+
cd ../lang && npm test
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
2. Ensure `npm link` for local dev (see TESTING.md)
|
|
14
|
+
|
|
15
|
+
## Version Bumps (for publish)
|
|
16
|
+
|
|
17
|
+
Suggested order (dependencies first):
|
|
18
|
+
|
|
19
|
+
- `@algosail/builtins` — bump if builtin sigs changed
|
|
20
|
+
- `@algosail/shared` — bump if API changed
|
|
21
|
+
- `@algosail/typecheck` — bump if API or behaviour changed
|
|
22
|
+
- `@algosail/parser` — bump if API changed
|
|
23
|
+
- `@algosail/compiler` — bump for source maps, JSDoc, async, error format
|
|
24
|
+
- `@algosail/lsp` — bump for hover, definition
|
|
25
|
+
- `@algosail/lang` — bump last; update deps to new versions
|
|
26
|
+
|
|
27
|
+
## Publish
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
cd <package> && npm publish --access public
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Post-release
|
|
34
|
+
|
|
35
|
+
- Update CHANGELOG.md with version and date
|
|
36
|
+
- Tag release if using git tags
|
package/docs/TESTING.md
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# Sail Testing
|
|
2
|
+
|
|
3
|
+
## Framework: Brittle
|
|
4
|
+
|
|
5
|
+
Sail packages use [brittle](https://github.com/holepunchto/brittle) for tests.
|
|
6
|
+
|
|
7
|
+
### Setup
|
|
8
|
+
|
|
9
|
+
```json
|
|
10
|
+
{
|
|
11
|
+
"scripts": {
|
|
12
|
+
"test": "brittle \"test/**/*.test.js\""
|
|
13
|
+
},
|
|
14
|
+
"devDependencies": {
|
|
15
|
+
"brittle": "^3.19.1"
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
### Test File Pattern
|
|
21
|
+
|
|
22
|
+
- Location: `test/**/*.test.js`
|
|
23
|
+
- Import: `import test from 'brittle'`
|
|
24
|
+
|
|
25
|
+
### Example
|
|
26
|
+
|
|
27
|
+
```javascript
|
|
28
|
+
import test from 'brittle'
|
|
29
|
+
import { checkWord } from '../lib/word/checkWord.js'
|
|
30
|
+
|
|
31
|
+
test('word step applies callee signature', (t) => {
|
|
32
|
+
const table = baseTable()
|
|
33
|
+
table.words.toInt = { ... }
|
|
34
|
+
const word = { ... }
|
|
35
|
+
t.alike(checkWord(word, table), [])
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
test('DUP builtin applies sig', (t) => {
|
|
39
|
+
// ...
|
|
40
|
+
t.alike(checkWord(word, table), [])
|
|
41
|
+
})
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Assertions
|
|
45
|
+
|
|
46
|
+
- `t.ok(value)` — truthy
|
|
47
|
+
- `t.absent(value)` — falsy
|
|
48
|
+
- `t.alike(actual, expected)` — deep equality
|
|
49
|
+
- `t.is(actual, expected)` — strict equality
|
|
50
|
+
|
|
51
|
+
### Running Tests
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
# In each package
|
|
55
|
+
cd typecheck && npm test
|
|
56
|
+
cd parser && npm test
|
|
57
|
+
cd compiler && npm test
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Local Package Linking (npm link)
|
|
61
|
+
|
|
62
|
+
Для локальной разработки связывай пакеты через `npm link`:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
# 1. В пакете-источнике (shared, builtins и т.д.)
|
|
66
|
+
cd shared && npm link
|
|
67
|
+
|
|
68
|
+
# 2. В пакете-потребителе (compiler, typecheck и т.д.)
|
|
69
|
+
cd ../compiler && npm link @algosail/shared
|
|
70
|
+
cd ../typecheck && npm link @algosail/shared
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Проверка: `ls -la node_modules/@algosail/shared` должен быть symlink.
|
|
74
|
+
|
|
75
|
+
### Integration Tests
|
|
76
|
+
|
|
77
|
+
Full pipeline (parse → typecheck → compile → run) is tested in `lang/test/integration.test.js`:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
cd lang && npm test
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Tests:
|
|
84
|
+
- Compile a simple program (builtins only), run via dynamic import, assert result
|
|
85
|
+
- Compile program with FFI (async_main.sail), run, assert result
|
|
86
|
+
- Invalid program (stack underflow) produces errors
|
|
File without changes
|
package/index.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@algosail/lang",
|
|
3
|
+
"version": "0.2.9",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"sail-typecheck": "cli/typecheck.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "brittle \"test/**/*.test.js\""
|
|
11
|
+
},
|
|
12
|
+
"devDependencies": {
|
|
13
|
+
"brittle": "^3.19.1"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@algosail/builtins": "^0.0.6",
|
|
17
|
+
"@algosail/compiler": "^0.0.2",
|
|
18
|
+
"@algosail/parser": "^0.1.1",
|
|
19
|
+
"@algosail/typecheck": "^0.1.1"
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration tests: parse → typecheck → compile → run
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import test from 'brittle'
|
|
6
|
+
import { compile } from '@algosail/compiler'
|
|
7
|
+
import { writeFile, unlink, readFile } from 'node:fs/promises'
|
|
8
|
+
import { tmpdir } from 'node:os'
|
|
9
|
+
import { join, dirname } from 'node:path'
|
|
10
|
+
import { pathToFileURL, fileURLToPath } from 'node:url'
|
|
11
|
+
import { randomUUID } from 'node:crypto'
|
|
12
|
+
|
|
13
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
14
|
+
|
|
15
|
+
test('full pipeline: compile simple program and run', async (t) => {
|
|
16
|
+
const source = `
|
|
17
|
+
@main ( Int -- Int )
|
|
18
|
+
DUP NIP
|
|
19
|
+
`
|
|
20
|
+
const uri = 'file:///test/integration.sail'
|
|
21
|
+
const { js, errors } = await compile(uri, source)
|
|
22
|
+
|
|
23
|
+
t.is(errors.length, 0, `expected no errors, got: ${JSON.stringify(errors)}`)
|
|
24
|
+
t.ok(js)
|
|
25
|
+
t.ok(js.includes('export function main'))
|
|
26
|
+
|
|
27
|
+
const tmpFile = join(tmpdir(), `sail-integration-${randomUUID()}.mjs`)
|
|
28
|
+
await writeFile(tmpFile, js, 'utf8')
|
|
29
|
+
try {
|
|
30
|
+
const mod = await import(pathToFileURL(tmpFile).href)
|
|
31
|
+
const result = mod.main(42)
|
|
32
|
+
t.is(result, 42, 'main(42) should return 42 (DUP NIP)')
|
|
33
|
+
} finally {
|
|
34
|
+
await unlink(tmpFile)
|
|
35
|
+
}
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
test('full pipeline: compile program with FFI', async (t) => {
|
|
39
|
+
const fixturesDir = join(__dirname, '..', '..', 'compiler', 'test', 'fixtures')
|
|
40
|
+
const uri = pathToFileURL(join(fixturesDir, 'async_main.sail')).href
|
|
41
|
+
const source = await readFile(join(fixturesDir, 'async_main.sail'), 'utf8')
|
|
42
|
+
|
|
43
|
+
const { js, errors } = await compile(uri, source, { outPath: fixturesDir })
|
|
44
|
+
|
|
45
|
+
t.is(errors.length, 0, `expected no errors, got: ${JSON.stringify(errors)}`)
|
|
46
|
+
t.ok(js, 'should emit JS')
|
|
47
|
+
t.ok(js.includes('export function main') || js.includes('export async function main'), 'should export main')
|
|
48
|
+
t.ok(/Async\.incAsync|incAsync/.test(js), 'should call FFI word')
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
test('full pipeline: typecheck rejects invalid program', async (t) => {
|
|
52
|
+
const source = `
|
|
53
|
+
@main ( Int -- Int )
|
|
54
|
+
DUP DROP POP
|
|
55
|
+
`
|
|
56
|
+
const uri = 'file:///test/invalid.sail'
|
|
57
|
+
const { js, errors } = await compile(uri, source)
|
|
58
|
+
|
|
59
|
+
t.ok(errors.length > 0, 'should have typecheck/stack errors')
|
|
60
|
+
t.ok(!js || errors.length > 0)
|
|
61
|
+
})
|