Compare commits
297 Commits
trampoline
...
mqbe
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b8b110b616 | ||
|
|
930dcfba36 | ||
|
|
eeccb3b34a | ||
|
|
407797881c | ||
|
|
7069475729 | ||
|
|
3e42c57479 | ||
|
|
4b76728230 | ||
|
|
4ff9332d38 | ||
|
|
27e852af5b | ||
|
|
66a44595c8 | ||
|
|
fc0a1547dc | ||
|
|
c0b4e70eb2 | ||
|
|
f4714b2b36 | ||
|
|
7f691fd52b | ||
|
|
d5209e1d59 | ||
|
|
68e2395b92 | ||
|
|
1b747720b7 | ||
|
|
849123d8fc | ||
|
|
6ad919624b | ||
|
|
a11f3e7d47 | ||
|
|
3d1fd37979 | ||
|
|
8fc9bfe013 | ||
|
|
368511f666 | ||
|
|
3934cdb683 | ||
|
|
45556c344d | ||
|
|
bc87fe5f70 | ||
|
|
790293d915 | ||
|
|
872cd6ab51 | ||
|
|
e04ab4c30c | ||
|
|
0503acb7e6 | ||
|
|
d0c68d7a7d | ||
|
|
7469383e66 | ||
|
|
1fee8f9f8b | ||
|
|
a4f3b025c5 | ||
|
|
d18ea1b330 | ||
|
|
4de0659474 | ||
|
|
27a9b72b07 | ||
|
|
a3622bd5bd | ||
|
|
2f6700415e | ||
|
|
243d92f7f3 | ||
|
|
8f9d026b9b | ||
|
|
2c9ac8f7b6 | ||
|
|
80f24e131f | ||
|
|
a8f8af7662 | ||
|
|
f5b3494762 | ||
|
|
13a6f6c79d | ||
|
|
1a925371d3 | ||
|
|
08d2bacb1f | ||
|
|
7322153e57 | ||
|
|
cc72c4cb0f | ||
|
|
ae1f09a28f | ||
|
|
3c842912a1 | ||
|
|
7cacf32078 | ||
|
|
b740612761 | ||
|
|
6001c2b4bb | ||
|
|
98625fa15b | ||
|
|
87fafa44c8 | ||
|
|
45ce76aef7 | ||
|
|
32fb44857c | ||
|
|
31d67f6710 | ||
|
|
bae4e957e9 | ||
|
|
3621b1ef33 | ||
|
|
836227c8d3 | ||
|
|
0ae59705d4 | ||
|
|
8e2607b6ca | ||
|
|
dc73e86d8c | ||
|
|
555cceb9d6 | ||
|
|
fbb7933eb6 | ||
|
|
0287d6ada4 | ||
|
|
73cd6a255d | ||
|
|
83ea67c01b | ||
|
|
16059cca4e | ||
|
|
9ffe60ebef | ||
|
|
2beafec5d9 | ||
|
|
aba8eb66bd | ||
|
|
1abcaa92c7 | ||
|
|
168f7c71d5 | ||
|
|
56ed895b6e | ||
|
|
1e4646999d | ||
|
|
68d6c907fe | ||
|
|
8150c64c7d | ||
|
|
024d796ca4 | ||
|
|
ea185dbffd | ||
|
|
6571262af0 | ||
|
|
77ae133747 | ||
|
|
142a2d518b | ||
|
|
5b65c64fe5 | ||
|
|
e985fa5fe1 | ||
|
|
160ade2410 | ||
|
|
e2bc5948c1 | ||
|
|
8cf98d8a9e | ||
|
|
3c38e828e5 | ||
|
|
af2d296f40 | ||
|
|
0a45394689 | ||
|
|
32885a422f | ||
|
|
8959e53303 | ||
|
|
8a9a02b131 | ||
|
|
f9d68b2990 | ||
|
|
017a57b1eb | ||
|
|
ff8c68d01c | ||
|
|
9212003401 | ||
|
|
f9f8a4db42 | ||
|
|
8db95c654b | ||
|
|
63feabed5d | ||
|
|
c814c0e1d8 | ||
|
|
bead0c48d4 | ||
|
|
98dcab4ba7 | ||
|
|
ae44ce7b4b | ||
|
|
1c38699b5a | ||
|
|
9a70a12d82 | ||
|
|
a8a271e014 | ||
|
|
91761c03e6 | ||
|
|
5a479cc765 | ||
|
|
97a003e025 | ||
|
|
20f14abd17 | ||
|
|
19ba184fec | ||
|
|
7909b11f6b | ||
|
|
27229c675c | ||
|
|
64d234ee35 | ||
|
|
e861d73eec | ||
|
|
a24331aae5 | ||
|
|
c1cb922b64 | ||
|
|
aacb0b48bf | ||
|
|
b38aec95b6 | ||
|
|
b29d3c2fe0 | ||
|
|
1cc3005b68 | ||
|
|
b86cd042fc | ||
|
|
8b7af0c22a | ||
|
|
f71f6a296b | ||
|
|
9bd764b11b | ||
|
|
058cdfd2e4 | ||
|
|
1ef837c6ff | ||
|
|
cd21de3d70 | ||
|
|
a98faa4dbb | ||
|
|
08559234c4 | ||
|
|
c3dc27eac6 | ||
|
|
7170a9c7eb | ||
|
|
a08ee50f84 | ||
|
|
ed7dd91c3f | ||
|
|
3abe20fee0 | ||
|
|
a92a96118e | ||
|
|
4e407fe301 | ||
|
|
ab74cdc173 | ||
|
|
2c9d039271 | ||
|
|
80d314c58f | ||
|
|
611fba2b6f | ||
|
|
f5fad52d47 | ||
|
|
2fc7d333ad | ||
|
|
d4635f2a75 | ||
|
|
19576533d9 | ||
|
|
c08249b6f1 | ||
|
|
dc348d023f | ||
|
|
fd5e4d155e | ||
|
|
e734353722 | ||
|
|
94f1645be1 | ||
|
|
41e3a6d91a | ||
|
|
04c569eab1 | ||
|
|
dc3c474b3a | ||
|
|
f1117bbd41 | ||
|
|
43faad95e0 | ||
|
|
acc9878b36 | ||
|
|
03c45ee8b0 | ||
|
|
a171a0d2af | ||
|
|
bb8d3930b3 | ||
|
|
522ae6128a | ||
|
|
16c26e4bf2 | ||
|
|
a9804785e0 | ||
|
|
11ae703693 | ||
|
|
f203278c3e | ||
|
|
a80557283a | ||
|
|
3e40885e07 | ||
|
|
e4b7de46f6 | ||
|
|
e680439a9b | ||
|
|
ae11504e00 | ||
|
|
893deaec23 | ||
|
|
69b032d3dc | ||
|
|
256a00c501 | ||
|
|
8e166b8f98 | ||
|
|
ddbdd00496 | ||
|
|
bdf0461e1f | ||
|
|
0b86af1d4c | ||
|
|
beac9608ea | ||
|
|
a04bebd0d7 | ||
|
|
ce74f726dd | ||
|
|
4d1ab60852 | ||
|
|
22ab6c8098 | ||
|
|
9a9775690f | ||
|
|
be71ae3bba | ||
|
|
f2a76cbb55 | ||
|
|
2d834c37b3 | ||
|
|
ba1b92aa78 | ||
|
|
c356fe462d | ||
|
|
b23b918f97 | ||
|
|
e720152bcd | ||
|
|
f093e6f5a3 | ||
|
|
4fc904de63 | ||
|
|
e53f55fb23 | ||
|
|
6150406905 | ||
|
|
a189440769 | ||
|
|
6c3c492446 | ||
|
|
bb83327a52 | ||
|
|
c74bee89a7 | ||
|
|
271a3d6724 | ||
|
|
c5ccc66e51 | ||
|
|
3a0ea31896 | ||
|
|
67e82fd12c | ||
|
|
3c59087c0c | ||
|
|
b79e07f57b | ||
|
|
fdcb374403 | ||
|
|
03feb370fd | ||
|
|
6712755940 | ||
|
|
b3f3bc8a5f | ||
|
|
a49b94e0a1 | ||
|
|
24ecff3f1c | ||
|
|
3ccaf68a5b | ||
|
|
64933260d4 | ||
|
|
561ab9d917 | ||
|
|
bcd6e641a5 | ||
|
|
2857581271 | ||
|
|
378ad6dc98 | ||
|
|
06f7791159 | ||
|
|
086508bacd | ||
|
|
8325253f1a | ||
|
|
802c94085b | ||
|
|
f9170b33e5 | ||
|
|
20f10ab887 | ||
|
|
d8b13548d2 | ||
|
|
0d93741c31 | ||
|
|
c6440ff98c | ||
|
|
a01b48dabc | ||
|
|
beea76949c | ||
|
|
36833db2c9 | ||
|
|
b7615bb801 | ||
|
|
e6838338fc | ||
|
|
4cf0ce00de | ||
|
|
0714017547 | ||
|
|
b60e79ccad | ||
|
|
420c2b859a | ||
|
|
6c1f53ec5f | ||
|
|
45d82438ca | ||
|
|
addb38da65 | ||
|
|
aa847ddf6e | ||
|
|
4da63db16e | ||
|
|
bfdd920178 | ||
|
|
26fce3a5a8 | ||
|
|
ef49606098 | ||
|
|
854d94e5c3 | ||
|
|
dc02d6899d | ||
|
|
b28ef39562 | ||
|
|
2841e91f40 | ||
|
|
8d601dfce3 | ||
|
|
2b60e3a242 | ||
|
|
823183c510 | ||
|
|
c051a99e75 | ||
|
|
b3c0837d49 | ||
|
|
ff18682485 | ||
|
|
9b3891c126 | ||
|
|
38a3697e28 | ||
|
|
cbf99295da | ||
|
|
5271688dd4 | ||
|
|
98cb2c3239 | ||
|
|
e695810e64 | ||
|
|
bbd2d298ba | ||
|
|
a7a323a74e | ||
|
|
97ece8e5cb | ||
|
|
45ee4a337c | ||
|
|
ce7d83ec91 | ||
|
|
b46406f755 | ||
|
|
ac91495679 | ||
|
|
5018901acb | ||
|
|
78a46b4a72 | ||
|
|
66a9ca27e2 | ||
|
|
ac4b47f075 | ||
|
|
6b9f75247e | ||
|
|
b039b0c4ba | ||
|
|
ffe7b61ae2 | ||
|
|
17b9aaaf51 | ||
|
|
5fd29366a6 | ||
|
|
f16586eaa2 | ||
|
|
86a70bce3a | ||
|
|
e04b15973a | ||
|
|
d044bde4f9 | ||
|
|
8403883b9d | ||
|
|
69245f82db | ||
|
|
8203f6d1c3 | ||
|
|
ef94b55058 | ||
|
|
3a3e77eccd | ||
|
|
438c90acb5 | ||
|
|
dd309b1a37 | ||
|
|
63cf76dcf9 | ||
|
|
df07069c38 | ||
|
|
eba0727247 | ||
|
|
b0f0a5f63f | ||
|
|
d12d77c22c | ||
|
|
d6468e7fd2 | ||
|
|
7ae5a0c06b | ||
|
|
0664c11af6 |
@@ -1,6 +0,0 @@
|
||||
[modules]
|
||||
[modules.extramath]
|
||||
hash = "MCLZT3JABTAENS4WVXKGWJ7JPBLZER4YQ5VN2PE7ZD2Z4WYGTIMA===="
|
||||
url = "https://gitea.pockle.world/john/extramath@master"
|
||||
downloaded = "Monday June 2 12:07:20.42 PM -5 2025 AD"
|
||||
commit = "84d81a19a8455bcf8dc494739e9e6d545df6ff2c"
|
||||
@@ -1,9 +1,20 @@
|
||||
BasedOnStyle: GNU
|
||||
Language: C
|
||||
|
||||
IndentWidth: 2
|
||||
TabWidth: 2
|
||||
UseTab: Never
|
||||
ContinuationIndentWidth: 2 # Indents continuation lines by 2 spaces
|
||||
ContinuationIndentWidth: 2
|
||||
|
||||
AllowShortFunctionsOnASingleLine: true
|
||||
AllowShortBlocksOnASingleLine: true
|
||||
AllowShortIfStatementsOnASingleLine: true
|
||||
BreakBeforeBraces: Attach
|
||||
ColumnLimit: 0
|
||||
BreakFunctionDefinitionParameters: false
|
||||
BinPackParameters: false
|
||||
BinPackArguments: false
|
||||
|
||||
# --- Fix the "static T\nname(...)" style ---
|
||||
AlwaysBreakAfterDefinitionReturnType: None
|
||||
BreakAfterReturnType: None
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,5 +1,7 @@
|
||||
.git/
|
||||
.obj/
|
||||
website/public/
|
||||
website/.hugo_build.lock
|
||||
bin/
|
||||
build/
|
||||
*.zip
|
||||
@@ -14,6 +16,7 @@ build/
|
||||
source/shaders/*.h
|
||||
.DS_Store
|
||||
*.html
|
||||
!website/themes/**/*.html
|
||||
.vscode
|
||||
*.icns
|
||||
icon.ico
|
||||
|
||||
123
CLAUDE.md
Normal file
123
CLAUDE.md
Normal file
@@ -0,0 +1,123 @@
|
||||
# ƿit (pit) Language Project
|
||||
|
||||
## Building
|
||||
|
||||
Recompile after changes: `make`
|
||||
Bootstrap from scratch (first time): `make bootstrap`
|
||||
Run `cell --help` to see all CLI flags.
|
||||
|
||||
## Code Style
|
||||
|
||||
All code uses 2 spaces for indentation. K&R style for C and Javascript.
|
||||
|
||||
## ƿit Script Quick Reference
|
||||
|
||||
ƿit script files: `.ce` (actors) and `.cm` (modules). The syntax is similar to JavaScript with important differences listed below.
|
||||
|
||||
### Key Differences from JavaScript
|
||||
|
||||
- `var` (mutable) and `def` (constant) — no `let` or `const`
|
||||
- `==` and `!=` are strict (no `===` or `!==`)
|
||||
- No `undefined` — only `null`
|
||||
- No classes — only objects and prototypes (`meme()`, `proto()`, `isa()`)
|
||||
- No `for...in`, `for...of`, spread (`...`), rest params, or default params
|
||||
- No named function declarations — use `var fn = function() {}` or arrow functions
|
||||
- Variables must be declared at function body level only (not in if/while/for/blocks)
|
||||
- All variables must be initialized at declaration (`var x` alone is an error; use `var x = null`)
|
||||
- No `try`/`catch`/`throw` — use `disrupt`/`disruption`
|
||||
- No arraybuffers — only `blob` (works with bits; must `stone(blob)` before reading)
|
||||
- Identifiers can contain `?` and `!` (e.g., `nil?`, `set!`, `is?valid`)
|
||||
- Prefer backticks for string interpolation; otherwise use `text()` to convert non-strings
|
||||
- Everything should be lowercase
|
||||
|
||||
### Intrinsic Functions (always available, no `use()` needed)
|
||||
|
||||
The creator functions are **polymorphic** — behavior depends on argument types:
|
||||
|
||||
- `array(number)` — create array of size N filled with null
|
||||
- `array(number, value_or_fn)` — create array with initial values
|
||||
- `array(array)` — copy array
|
||||
- `array(array, fn)` — map
|
||||
- `array(array, array)` — concatenate
|
||||
- `array(array, from, to)` — slice
|
||||
- `array(record)` — get keys as array of text
|
||||
- **`array(text)` — split text into individual characters** (e.g., `array("hello")` → `["h","e","l","l","o"]`)
|
||||
- `array(text, separator)` — split by separator
|
||||
- `array(text, length)` — split into chunks of length
|
||||
|
||||
- `text(array, separator)` — join array into text
|
||||
- `text(number)` or `text(number, radix)` — number to text
|
||||
- `text(text, from, to)` — substring
|
||||
|
||||
- `number(text)` or `number(text, radix)` — parse text to number
|
||||
- `number(logical)` — boolean to number
|
||||
|
||||
- `record(record)` — copy
|
||||
- `record(record, another)` — merge
|
||||
- `record(array_of_keys)` — create record from keys
|
||||
|
||||
Other key intrinsics: `length()`, `stone()`, `is_stone()`, `print()`, `filter()`, `find()`, `reduce()`, `sort()`, `reverse()`, `some()`, `every()`, `starts_with()`, `ends_with()`, `meme()`, `proto()`, `isa()`, `splat()`, `apply()`, `extract()`, `replace()`, `search()`, `format()`, `lower()`, `upper()`, `trim()`
|
||||
|
||||
Sensory functions: `is_array()`, `is_text()`, `is_number()`, `is_object()`, `is_function()`, `is_null()`, `is_logical()`, `is_integer()`, `is_stone()`, etc.
|
||||
|
||||
### Standard Library (loaded with `use()`)
|
||||
|
||||
- `blob` — binary data (bits, not bytes)
|
||||
- `time` — time constants and conversions
|
||||
- `math` — trig, logarithms, roots (`math/radians`, `math/turns`)
|
||||
- `json` — JSON encoding/decoding
|
||||
- `random` — random number generation
|
||||
|
||||
### Actor Model
|
||||
|
||||
- `.ce` files are actors (independent execution units, don't return values)
|
||||
- `.cm` files are modules (return a value, cached and frozen)
|
||||
- Actors never share memory; communicate via `$send()` message passing
|
||||
- Actor intrinsics start with `$`: `$me`, `$stop()`, `$send()`, `$start()`, `$delay()`, `$receiver()`, `$clock()`, `$portal()`, `$contact()`, `$couple()`, `$unneeded()`, `$connection()`, `$time_limit()`
|
||||
|
||||
### Requestors (async composition)
|
||||
|
||||
`sequence()`, `parallel()`, `race()`, `fallback()` — compose asynchronous operations. See docs/requestors.md.
|
||||
|
||||
### Error Handling
|
||||
|
||||
```javascript
|
||||
var fn = function() {
|
||||
disrupt // bare keyword, no value
|
||||
} disruption {
|
||||
// handle error; can re-raise with disrupt
|
||||
}
|
||||
```
|
||||
|
||||
### Push/Pop Syntax
|
||||
|
||||
```javascript
|
||||
var a = [1, 2]
|
||||
a[] = 3 // push: [1, 2, 3]
|
||||
var v = a[] // pop: v is 3, a is [1, 2]
|
||||
```
|
||||
|
||||
## C Integration
|
||||
|
||||
- Declare everything `static` that can be
|
||||
- Most files don't have headers; files in a package are not shared between packages
|
||||
- No undefined in C API: use `JS_IsNull` and `JS_NULL` only
|
||||
- A C file with correct macros (`CELL_USE_FUNCS` etc) is loaded as a module by its name (e.g., `png.c` in a package → `use('<package>/png')`)
|
||||
|
||||
## Project Layout
|
||||
|
||||
- `source/` — C source for the cell runtime and CLI
|
||||
- `docs/` — master documentation (Markdown), reflected on the website
|
||||
- `website/` — Hugo site; theme at `website/themes/knr/`
|
||||
- `internal/` — internal ƿit scripts (engine.cm etc.)
|
||||
- `packages/` — core packages
|
||||
- `Makefile` — build system (`make` to rebuild, `make bootstrap` for first build)
|
||||
|
||||
## Documentation
|
||||
|
||||
The `docs/` folder is the single source of truth. The website at `website/` mounts it via Hugo. Key files:
|
||||
- `docs/language.md` — language syntax reference
|
||||
- `docs/functions.md` — all built-in intrinsic functions
|
||||
- `docs/actors.md` — actor model and actor intrinsics
|
||||
- `docs/requestors.md` — async requestor pattern
|
||||
- `docs/library/*.md` — intrinsic type reference (text, number, array, object) and standard library modules
|
||||
2
Makefile
2
Makefile
@@ -9,6 +9,8 @@
|
||||
CELL_SHOP = $(HOME)/.cell
|
||||
CELL_CORE_PACKAGE = $(CELL_SHOP)/packages/core
|
||||
|
||||
maker: install
|
||||
|
||||
makecell:
|
||||
cell pack core -o cell
|
||||
cp cell /opt/homebrew/bin/
|
||||
|
||||
99
add.ce
99
add.ce
@@ -1,28 +1,103 @@
|
||||
// cell add <locator> [alias] - Add and install a package with its dependencies
|
||||
// cell add <locator> [alias] - Add a dependency to the current package
|
||||
//
|
||||
// Usage:
|
||||
// cell add <locator> Add a dependency using default alias
|
||||
// cell add <locator> <alias> Add a dependency with custom alias
|
||||
//
|
||||
// This adds the dependency to cell.toml and installs it to the shop.
|
||||
|
||||
var shop = use('internal/shop')
|
||||
var pkg = use('package')
|
||||
var build = use('build')
|
||||
var fd = use('fd')
|
||||
|
||||
if (args.length < 1) {
|
||||
var locator = null
|
||||
var alias = null
|
||||
|
||||
array(args, function(arg) {
|
||||
if (arg == '--help' || arg == '-h') {
|
||||
log.console("Usage: cell add <locator> [alias]")
|
||||
log.console("")
|
||||
log.console("Add a dependency to the current package.")
|
||||
log.console("")
|
||||
log.console("Examples:")
|
||||
log.console(" cell add gitea.pockle.world/john/prosperon")
|
||||
log.console(" cell add gitea.pockle.world/john/cell-image image")
|
||||
log.console(" cell add ../local-package")
|
||||
$stop()
|
||||
} else if (!starts_with(arg, '-')) {
|
||||
if (!locator) {
|
||||
locator = arg
|
||||
} else if (!alias) {
|
||||
alias = arg
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if (!locator) {
|
||||
log.console("Usage: cell add <locator> [alias]")
|
||||
log.console("Examples:")
|
||||
log.console(" cell add gitea.pockle.world/john/prosperon@main")
|
||||
log.console(" cell add github.com/user/repo@v1.0.0 myalias")
|
||||
$stop()
|
||||
return
|
||||
}
|
||||
|
||||
var locator = args[0]
|
||||
|
||||
// Resolve relative paths to absolute paths
|
||||
if (locator == '.' || locator.startsWith('./') || locator.startsWith('../') || fd.is_dir(locator)) {
|
||||
if (locator == '.' || starts_with(locator, './') || starts_with(locator, '../') || fd.is_dir(locator)) {
|
||||
var resolved = fd.realpath(locator)
|
||||
if (resolved) {
|
||||
locator = resolved
|
||||
}
|
||||
}
|
||||
var alias = args.length > 1 ? args[1] : null
|
||||
|
||||
shop.get(locator, alias)
|
||||
// Generate default alias from locator
|
||||
if (!alias) {
|
||||
// Use the last component of the locator as alias
|
||||
var parts = array(locator, '/')
|
||||
alias = parts[length(parts) - 1]
|
||||
// Remove any version suffix
|
||||
if (search(alias, '@') != null) {
|
||||
alias = array(alias, '@')[0]
|
||||
}
|
||||
}
|
||||
|
||||
$stop()
|
||||
// Check we're in a package directory
|
||||
var cwd = fd.realpath('.')
|
||||
if (!fd.is_file(cwd + '/cell.toml')) {
|
||||
log.error("Not in a package directory (no cell.toml found)")
|
||||
$stop()
|
||||
}
|
||||
|
||||
log.console("Adding " + locator + " as '" + alias + "'...")
|
||||
|
||||
// Add to local project's cell.toml
|
||||
try {
|
||||
pkg.add_dependency(null, locator, alias)
|
||||
log.console(" Added to cell.toml")
|
||||
} catch (e) {
|
||||
log.error("Failed to update cell.toml: " + e)
|
||||
$stop()
|
||||
}
|
||||
|
||||
// Install to shop
|
||||
try {
|
||||
shop.get(locator)
|
||||
shop.extract(locator)
|
||||
|
||||
// Build scripts
|
||||
shop.build_package_scripts(locator)
|
||||
|
||||
// Build C code if any
|
||||
try {
|
||||
var target = build.detect_host_target()
|
||||
build.build_dynamic(locator, target, 'release')
|
||||
} catch (e) {
|
||||
// Not all packages have C code
|
||||
}
|
||||
|
||||
log.console(" Installed to shop")
|
||||
} catch (e) {
|
||||
log.error("Failed to install: " + e)
|
||||
$stop()
|
||||
}
|
||||
|
||||
log.console("Added " + alias + " (" + locator + ")")
|
||||
|
||||
$stop()
|
||||
|
||||
@@ -8,14 +8,14 @@ static JSClassID js_writer_class_id;
|
||||
static void js_reader_finalizer(JSRuntime *rt, JSValue val) {
|
||||
mz_zip_archive *zip = JS_GetOpaque(val, js_reader_class_id);
|
||||
mz_zip_reader_end(zip);
|
||||
js_free_rt(rt,zip);
|
||||
js_free_rt(zip);
|
||||
}
|
||||
|
||||
static void js_writer_finalizer(JSRuntime *rt, JSValue val) {
|
||||
mz_zip_archive *zip = JS_GetOpaque(val, js_writer_class_id);
|
||||
mz_zip_writer_finalize_archive(zip);
|
||||
mz_zip_writer_end(zip);
|
||||
js_free_rt(rt,zip);
|
||||
js_free_rt(zip);
|
||||
}
|
||||
|
||||
static JSClassDef js_reader_class = {
|
||||
@@ -101,7 +101,7 @@ static JSValue js_miniz_compress(JSContext *js, JSValue this_val,
|
||||
size_t in_len = 0;
|
||||
const void *in_ptr = NULL;
|
||||
|
||||
if (JS_IsString(argv[0])) {
|
||||
if (JS_IsText(argv[0])) {
|
||||
/* String → UTF-8 bytes without the terminating NUL */
|
||||
cstring = JS_ToCStringLen(js, &in_len, argv[0]);
|
||||
if (!cstring)
|
||||
@@ -382,13 +382,13 @@ static const JSCFunctionListEntry js_reader_funcs[] = {
|
||||
JSValue js_miniz_use(JSContext *js)
|
||||
{
|
||||
JS_NewClassID(&js_reader_class_id);
|
||||
JS_NewClass(JS_GetRuntime(js), js_reader_class_id, &js_reader_class);
|
||||
JS_NewClass(js, js_reader_class_id, &js_reader_class);
|
||||
JSValue reader_proto = JS_NewObject(js);
|
||||
JS_SetPropertyFunctionList(js, reader_proto, js_reader_funcs, sizeof(js_reader_funcs) / sizeof(JSCFunctionListEntry));
|
||||
JS_SetClassProto(js, js_reader_class_id, reader_proto);
|
||||
|
||||
JS_NewClassID(&js_writer_class_id);
|
||||
JS_NewClass(JS_GetRuntime(js), js_writer_class_id, &js_writer_class);
|
||||
JS_NewClass(js, js_writer_class_id, &js_writer_class);
|
||||
JSValue writer_proto = JS_NewObject(js);
|
||||
JS_SetPropertyFunctionList(js, writer_proto, js_writer_funcs, sizeof(js_writer_funcs) / sizeof(JSCFunctionListEntry));
|
||||
JS_SetClassProto(js, js_writer_class_id, writer_proto);
|
||||
|
||||
261
bench.ce
261
bench.ce
@@ -24,63 +24,45 @@ def MAX_BATCH_SIZE = 100000000 // 100M iterations max per batch
|
||||
|
||||
// Statistical functions
|
||||
function median(arr) {
|
||||
if (arr.length == 0) return 0
|
||||
var sorted = arr.slice().sort(function(a, b) { return a - b })
|
||||
var mid = number.floor(arr.length / 2)
|
||||
if (arr.length % 2 == 0) {
|
||||
if (length(arr) == 0) return 0
|
||||
var sorted = sort(arr)
|
||||
var mid = floor(length(arr) / 2)
|
||||
if (length(arr) % 2 == 0) {
|
||||
return (sorted[mid - 1] + sorted[mid]) / 2
|
||||
}
|
||||
return sorted[mid]
|
||||
}
|
||||
|
||||
function mean(arr) {
|
||||
if (arr.length == 0) return 0
|
||||
if (length(arr) == 0) return 0
|
||||
var sum = 0
|
||||
for (var i = 0; i < arr.length; i++) {
|
||||
sum += arr[i]
|
||||
}
|
||||
return sum / arr.length
|
||||
arrfor(arr, function(val) {
|
||||
sum += val
|
||||
})
|
||||
return sum / length(arr)
|
||||
}
|
||||
|
||||
function stddev(arr, mean_val) {
|
||||
if (arr.length < 2) return 0
|
||||
if (length(arr) < 2) return 0
|
||||
var sum_sq_diff = 0
|
||||
for (var i = 0; i < arr.length; i++) {
|
||||
var diff = arr[i] - mean_val
|
||||
arrfor(arr, function(val) {
|
||||
var diff = val - mean_val
|
||||
sum_sq_diff += diff * diff
|
||||
}
|
||||
return math.sqrt(sum_sq_diff / (arr.length - 1))
|
||||
})
|
||||
return math.sqrt(sum_sq_diff / (length(arr) - 1))
|
||||
}
|
||||
|
||||
function percentile(arr, p) {
|
||||
if (arr.length == 0) return 0
|
||||
var sorted = arr.slice().sort(function(a, b) { return a - b })
|
||||
var idx = number.floor(arr.length * p / 100)
|
||||
if (idx >= arr.length) idx = arr.length - 1
|
||||
if (length(arr) == 0) return 0
|
||||
var sorted = sort(arr)
|
||||
var idx = floor(arr) * p / 100
|
||||
if (idx >= length(arr)) idx = length(arr) - 1
|
||||
return sorted[idx]
|
||||
}
|
||||
|
||||
function min_val(arr) {
|
||||
if (arr.length == 0) return 0
|
||||
var m = arr[0]
|
||||
for (var i = 1; i < arr.length; i++) {
|
||||
if (arr[i] < m) m = arr[i]
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
function max_val(arr) {
|
||||
if (arr.length == 0) return 0
|
||||
var m = arr[0]
|
||||
for (var i = 1; i < arr.length; i++) {
|
||||
if (arr[i] > m) m = arr[i]
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// Parse arguments similar to test.ce
|
||||
function parse_args() {
|
||||
if (args.length == 0) {
|
||||
if (length(args) == 0) {
|
||||
if (!testlib.is_valid_package('.')) {
|
||||
log.console('No cell.toml found in current directory')
|
||||
return false
|
||||
@@ -99,7 +81,7 @@ function parse_args() {
|
||||
}
|
||||
|
||||
if (args[0] == 'package') {
|
||||
if (args.length < 2) {
|
||||
if (length(args) < 2) {
|
||||
log.console('Usage: cell bench package <name> [bench]')
|
||||
log.console(' cell bench package all')
|
||||
return false
|
||||
@@ -115,7 +97,7 @@ function parse_args() {
|
||||
var lock = shop.load_lock()
|
||||
if (lock[name]) {
|
||||
target_pkg = name
|
||||
} else if (name.startsWith('/') && testlib.is_valid_package(name)) {
|
||||
} else if (starts_with(name, '/') && testlib.is_valid_package(name)) {
|
||||
target_pkg = name
|
||||
} else {
|
||||
if (testlib.is_valid_package('.')) {
|
||||
@@ -132,7 +114,7 @@ function parse_args() {
|
||||
}
|
||||
}
|
||||
|
||||
if (args.length >= 3) {
|
||||
if (length(args) >= 3) {
|
||||
target_bench = args[2]
|
||||
}
|
||||
|
||||
@@ -144,7 +126,7 @@ function parse_args() {
|
||||
var bench_path = args[0]
|
||||
|
||||
// Normalize path - add benches/ prefix if not present
|
||||
if (!bench_path.startsWith('benches/') && !bench_path.startsWith('/')) {
|
||||
if (!starts_with(bench_path, 'benches/') && !starts_with(bench_path, '/')) {
|
||||
if (!fd.is_file(bench_path + '.cm') && !fd.is_file(bench_path)) {
|
||||
if (fd.is_file('benches/' + bench_path + '.cm') || fd.is_file('benches/' + bench_path)) {
|
||||
bench_path = 'benches/' + bench_path
|
||||
@@ -177,19 +159,18 @@ function collect_benches(package_name, specific_bench) {
|
||||
|
||||
var files = pkg.list_files(package_name)
|
||||
var bench_files = []
|
||||
for (var i = 0; i < files.length; i++) {
|
||||
var f = files[i]
|
||||
if (f.startsWith("benches/") && f.endsWith(".cm")) {
|
||||
arrfor(files, function(f) {
|
||||
if (starts_with(f, "benches/") && ends_with(f, ".cm")) {
|
||||
if (specific_bench) {
|
||||
var bench_name = f.substring(0, f.length - 3)
|
||||
var bench_name = text(f, 0, -3)
|
||||
var match_name = specific_bench
|
||||
if (!match_name.startsWith('benches/')) match_name = 'benches/' + match_name
|
||||
var match_base = match_name.endsWith('.cm') ? match_name.substring(0, match_name.length - 3) : match_name
|
||||
if (bench_name != match_base) continue
|
||||
if (!starts_with(match_name, 'benches/')) match_name = 'benches/' + match_name
|
||||
var match_base = ends_with(match_name, '.cm') ? text(match_name, 0, -3) : match_name
|
||||
if (bench_name != match_base) return
|
||||
}
|
||||
bench_files.push(f)
|
||||
push(bench_files, f)
|
||||
}
|
||||
}
|
||||
})
|
||||
return bench_files
|
||||
}
|
||||
|
||||
@@ -203,7 +184,7 @@ function calibrate_batch_size(bench_fn, is_batch) {
|
||||
// Find a batch size that takes at least MIN_SAMPLE_NS
|
||||
while (n < MAX_BATCH_SIZE) {
|
||||
// Ensure n is a valid number before calling
|
||||
if (typeof n != 'number' || n < 1) {
|
||||
if (!is_number(n) || n < 1) {
|
||||
n = 1
|
||||
break
|
||||
}
|
||||
@@ -217,7 +198,7 @@ function calibrate_batch_size(bench_fn, is_batch) {
|
||||
// Double the batch size
|
||||
var new_n = n * 2
|
||||
// Check if multiplication produced a valid number
|
||||
if (typeof new_n != 'number' || new_n > MAX_BATCH_SIZE) {
|
||||
if (!is_number(new_n) || new_n > MAX_BATCH_SIZE) {
|
||||
n = MAX_BATCH_SIZE
|
||||
break
|
||||
}
|
||||
@@ -225,12 +206,12 @@ function calibrate_batch_size(bench_fn, is_batch) {
|
||||
}
|
||||
|
||||
// Adjust to target sample duration
|
||||
if (dt > 0 && dt < TARGET_SAMPLE_NS && typeof n == 'number' && typeof dt == 'number') {
|
||||
if (dt > 0 && dt < TARGET_SAMPLE_NS && is_number(n) && is_number(dt)) {
|
||||
var calc = n * TARGET_SAMPLE_NS / dt
|
||||
if (typeof calc == 'number' && calc > 0) {
|
||||
var target_n = number.floor(calc)
|
||||
if (is_number(calc) && calc > 0) {
|
||||
var target_n = floor(calc)
|
||||
// Check if floor returned a valid number
|
||||
if (typeof target_n == 'number' && target_n > 0) {
|
||||
if (is_number(target_n) && target_n > 0) {
|
||||
if (target_n > MAX_BATCH_SIZE) target_n = MAX_BATCH_SIZE
|
||||
if (target_n < MIN_BATCH_SIZE) target_n = MIN_BATCH_SIZE
|
||||
n = target_n
|
||||
@@ -239,7 +220,7 @@ function calibrate_batch_size(bench_fn, is_batch) {
|
||||
}
|
||||
|
||||
// Safety check - ensure we always return a valid batch size
|
||||
if (typeof n != 'number' || n < 1) {
|
||||
if (!is_number(n) || n < 1) {
|
||||
n = 1
|
||||
}
|
||||
|
||||
@@ -254,7 +235,7 @@ function run_single_bench(bench_fn, bench_name) {
|
||||
// 1. Object with { setup, run, teardown } - structured format
|
||||
// 2. Function that accepts (n) - batch format
|
||||
// 3. Function that accepts () - legacy format
|
||||
var is_structured = typeof bench_fn == 'object' && bench_fn.run
|
||||
var is_structured = is_object(bench_fn) && bench_fn.run
|
||||
var is_batch = false
|
||||
var batch_size = 1
|
||||
var setup_fn = null
|
||||
@@ -285,7 +266,7 @@ function run_single_bench(bench_fn, bench_name) {
|
||||
batch_size = calibrate_batch_size(calibrate_fn, is_batch)
|
||||
|
||||
// Safety check for structured benchmarks
|
||||
if (typeof batch_size != 'number' || batch_size < 1) {
|
||||
if (!is_number(batch_size) || batch_size < 1) {
|
||||
batch_size = 1
|
||||
}
|
||||
} else {
|
||||
@@ -307,8 +288,9 @@ function run_single_bench(bench_fn, bench_name) {
|
||||
// Warmup phase
|
||||
for (var i = 0; i < WARMUP_BATCHES; i++) {
|
||||
// Ensure batch_size is valid before warmup
|
||||
if (typeof batch_size != 'number' || batch_size < 1) {
|
||||
log.console(`WARNING: batch_size became ${typeof batch_size} = ${batch_size}, resetting to 1`)
|
||||
if (!is_number(batch_size) || batch_size < 1) {
|
||||
var type_str = is_null(batch_size) ? 'null' : is_number(batch_size) ? 'number' : is_text(batch_size) ? 'text' : is_object(batch_size) ? 'object' : is_array(batch_size) ? 'array' : is_function(batch_size) ? 'function' : is_logical(batch_size) ? 'logical' : 'unknown'
|
||||
log.console(`WARNING: batch_size became ${type_str} = ${batch_size}, resetting to 1`)
|
||||
batch_size = 1
|
||||
}
|
||||
|
||||
@@ -332,7 +314,7 @@ function run_single_bench(bench_fn, bench_name) {
|
||||
// Measurement phase - collect SAMPLES timing samples
|
||||
for (var i = 0; i < SAMPLES; i++) {
|
||||
// Double-check batch_size is valid (should never happen, but defensive)
|
||||
if (typeof batch_size != 'number' || batch_size < 1) {
|
||||
if (!is_number(batch_size) || batch_size < 1) {
|
||||
batch_size = 1
|
||||
}
|
||||
|
||||
@@ -348,7 +330,7 @@ function run_single_bench(bench_fn, bench_name) {
|
||||
if (teardown_fn) teardown_fn(state)
|
||||
|
||||
var ns_per_op = is_batch ? duration / batch_size : duration
|
||||
timings_per_op.push(ns_per_op)
|
||||
push(timings_per_op, ns_per_op)
|
||||
} else {
|
||||
var start = os.now()
|
||||
if (is_batch) {
|
||||
@@ -359,15 +341,15 @@ function run_single_bench(bench_fn, bench_name) {
|
||||
var duration = os.now() - start
|
||||
|
||||
var ns_per_op = is_batch ? duration / batch_size : duration
|
||||
timings_per_op.push(ns_per_op)
|
||||
push(timings_per_op, ns_per_op)
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate statistics
|
||||
var mean_ns = mean(timings_per_op)
|
||||
var median_ns = median(timings_per_op)
|
||||
var min_ns = min_val(timings_per_op)
|
||||
var max_ns = max_val(timings_per_op)
|
||||
var min_ns = reduce(timings_per_op, min)
|
||||
var max_ns = reduce(timings_per_op, max)
|
||||
var stddev_ns = stddev(timings_per_op, mean_ns)
|
||||
var p95_ns = percentile(timings_per_op, 95)
|
||||
var p99_ns = percentile(timings_per_op, 99)
|
||||
@@ -375,20 +357,20 @@ function run_single_bench(bench_fn, bench_name) {
|
||||
// Calculate ops/s from median
|
||||
var ops_per_sec = 0
|
||||
if (median_ns > 0) {
|
||||
ops_per_sec = number.floor(1000000000 / median_ns)
|
||||
ops_per_sec = floor(1000000000 / median_ns)
|
||||
}
|
||||
|
||||
return {
|
||||
name: bench_name,
|
||||
batch_size: batch_size,
|
||||
samples: SAMPLES,
|
||||
mean_ns: number.round(mean_ns),
|
||||
median_ns: number.round(median_ns),
|
||||
min_ns: number.round(min_ns),
|
||||
max_ns: number.round(max_ns),
|
||||
stddev_ns: number.round(stddev_ns),
|
||||
p95_ns: number.round(p95_ns),
|
||||
p99_ns: number.round(p99_ns),
|
||||
mean_ns: round(mean_ns),
|
||||
median_ns: round(median_ns),
|
||||
min_ns: round(min_ns),
|
||||
max_ns: round(max_ns),
|
||||
stddev_ns: round(stddev_ns),
|
||||
p95_ns: round(p95_ns),
|
||||
p99_ns: round(p99_ns),
|
||||
ops_per_sec: ops_per_sec
|
||||
}
|
||||
}
|
||||
@@ -396,17 +378,17 @@ function run_single_bench(bench_fn, bench_name) {
|
||||
// Format nanoseconds for display
|
||||
function format_ns(ns) {
|
||||
if (ns < 1000) return `${ns}ns`
|
||||
if (ns < 1000000) return `${number.round(ns / 1000 * 100) / 100}µs`
|
||||
if (ns < 1000000000) return `${number.round(ns / 1000000 * 100) / 100}ms`
|
||||
return `${number.round(ns / 1000000000 * 100) / 100}s`
|
||||
if (ns < 1000000) return `${round(ns / 1000 * 100) / 100}µs`
|
||||
if (ns < 1000000000) return `${round(ns / 1000000 * 100) / 100}ms`
|
||||
return `${round(ns / 1000000000 * 100) / 100}s`
|
||||
}
|
||||
|
||||
// Format ops/sec for display
|
||||
function format_ops(ops) {
|
||||
if (ops < 1000) return `${ops} ops/s`
|
||||
if (ops < 1000000) return `${number.round(ops / 1000 * 100) / 100}K ops/s`
|
||||
if (ops < 1000000000) return `${number.round(ops / 1000000 * 100) / 100}M ops/s`
|
||||
return `${number.round(ops / 1000000000 * 100) / 100}G ops/s`
|
||||
if (ops < 1000000) return `${round(ops / 1000 * 100) / 100}K ops/s`
|
||||
if (ops < 1000000000) return `${round(ops / 1000000 * 100) / 100}M ops/s`
|
||||
return `${round(ops / 1000000000 * 100) / 100}G ops/s`
|
||||
}
|
||||
|
||||
// Run benchmarks for a package
|
||||
@@ -419,14 +401,13 @@ function run_benchmarks(package_name, specific_bench) {
|
||||
total: 0
|
||||
}
|
||||
|
||||
if (bench_files.length == 0) return pkg_result
|
||||
if (length(bench_files) == 0) return pkg_result
|
||||
|
||||
if (package_name) log.console(`Running benchmarks for ${package_name}`)
|
||||
else log.console(`Running benchmarks for local package`)
|
||||
|
||||
for (var i = 0; i < bench_files.length; i++) {
|
||||
var f = bench_files[i]
|
||||
var mod_path = f.substring(0, f.length - 3)
|
||||
arrfor(bench_files, function(f) {
|
||||
var mod_path = text(f, 0, -3)
|
||||
|
||||
var file_result = {
|
||||
name: f,
|
||||
@@ -439,24 +420,22 @@ function run_benchmarks(package_name, specific_bench) {
|
||||
bench_mod = shop.use(mod_path, use_pkg)
|
||||
|
||||
var benches = []
|
||||
if (typeof bench_mod == 'function') {
|
||||
benches.push({name: 'main', fn: bench_mod})
|
||||
} else if (typeof bench_mod == 'object') {
|
||||
for (var k in bench_mod) {
|
||||
if (typeof bench_mod[k] == 'function') {
|
||||
benches.push({name: k, fn: bench_mod[k]})
|
||||
}
|
||||
}
|
||||
if (is_function(bench_mod)) {
|
||||
push(benches, {name: 'main', fn: bench_mod})
|
||||
} else if (is_object(bench_mod)) {
|
||||
arrfor(array(bench_mod), function(k) {
|
||||
if (is_function(bench_mod[k]))
|
||||
push(benches, {name: k, fn: bench_mod[k]})
|
||||
})
|
||||
}
|
||||
|
||||
if (benches.length > 0) {
|
||||
if (length(benches) > 0) {
|
||||
log.console(` ${f}`)
|
||||
for (var j = 0; j < benches.length; j++) {
|
||||
var b = benches[j]
|
||||
arrfor(benches, function(b) {
|
||||
try {
|
||||
var result = run_single_bench(b.fn, b.name)
|
||||
result.package = pkg_result.package
|
||||
file_result.benchmarks.push(result)
|
||||
push(file_result.benchmarks, result)
|
||||
pkg_result.total++
|
||||
|
||||
log.console(` ${result.name}`)
|
||||
@@ -473,10 +452,10 @@ function run_benchmarks(package_name, specific_bench) {
|
||||
name: b.name,
|
||||
error: e.toString()
|
||||
}
|
||||
file_result.benchmarks.push(error_result)
|
||||
push(file_result.benchmarks, error_result)
|
||||
pkg_result.total++
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
log.console(` Error loading ${f}: ${e}`)
|
||||
@@ -485,14 +464,14 @@ function run_benchmarks(package_name, specific_bench) {
|
||||
name: "load_module",
|
||||
error: `Error loading module: ${e}`
|
||||
}
|
||||
file_result.benchmarks.push(error_result)
|
||||
push(file_result.benchmarks, error_result)
|
||||
pkg_result.total++
|
||||
}
|
||||
|
||||
if (file_result.benchmarks.length > 0) {
|
||||
pkg_result.files.push(file_result)
|
||||
if (length(file_result.benchmarks) > 0) {
|
||||
push(pkg_result.files, file_result)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return pkg_result
|
||||
}
|
||||
@@ -502,29 +481,29 @@ var all_results = []
|
||||
|
||||
if (all_pkgs) {
|
||||
if (testlib.is_valid_package('.')) {
|
||||
all_results.push(run_benchmarks(null, null))
|
||||
push(all_results, run_benchmarks(null, null))
|
||||
}
|
||||
|
||||
var packages = shop.list_packages()
|
||||
for (var i = 0; i < packages.length; i++) {
|
||||
all_results.push(run_benchmarks(packages[i], null))
|
||||
}
|
||||
arrfor(packages, function(pkg) {
|
||||
push(all_results, run_benchmarks(pkg, null))
|
||||
})
|
||||
} else {
|
||||
all_results.push(run_benchmarks(target_pkg, target_bench))
|
||||
push(all_results, run_benchmarks(target_pkg, target_bench))
|
||||
}
|
||||
|
||||
// Calculate totals
|
||||
var total_benches = 0
|
||||
for (var i = 0; i < all_results.length; i++) {
|
||||
total_benches += all_results[i].total
|
||||
}
|
||||
arrfor(all_results, function(result) {
|
||||
total_benches += result.total
|
||||
})
|
||||
|
||||
log.console(`----------------------------------------`)
|
||||
log.console(`Benchmarks: ${total_benches} total`)
|
||||
|
||||
// Generate reports
|
||||
function generate_reports() {
|
||||
var timestamp = text(number.floor(time.number()))
|
||||
var timestamp = text(floor(time.number()))
|
||||
var report_dir = shop.get_reports_dir() + '/bench_' + timestamp
|
||||
testlib.ensure_dir(report_dir)
|
||||
|
||||
@@ -534,34 +513,28 @@ Total benchmarks: ${total_benches}
|
||||
|
||||
=== SUMMARY ===
|
||||
`
|
||||
for (var i = 0; i < all_results.length; i++) {
|
||||
var pkg_res = all_results[i]
|
||||
if (pkg_res.total == 0) continue
|
||||
arrfor(all_results, function(pkg_res) {
|
||||
if (pkg_res.total == 0) return
|
||||
txt_report += `Package: ${pkg_res.package}\n`
|
||||
for (var j = 0; j < pkg_res.files.length; j++) {
|
||||
var f = pkg_res.files[j]
|
||||
arrfor(pkg_res.files, function(f) {
|
||||
txt_report += ` ${f.name}\n`
|
||||
for (var k = 0; k < f.benchmarks.length; k++) {
|
||||
var b = f.benchmarks[k]
|
||||
arrfor(f.benchmarks, function(b) {
|
||||
if (b.error) {
|
||||
txt_report += ` ERROR ${b.name}: ${b.error}\n`
|
||||
} else {
|
||||
txt_report += ` ${b.name}: ${format_ns(b.median_ns)}/op (${format_ops(b.ops_per_sec)})\n`
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
txt_report += `\n=== DETAILED RESULTS ===\n`
|
||||
for (var i = 0; i < all_results.length; i++) {
|
||||
var pkg_res = all_results[i]
|
||||
if (pkg_res.total == 0) continue
|
||||
arrfor(all_results, function(pkg_res) {
|
||||
if (pkg_res.total == 0) return
|
||||
|
||||
for (var j = 0; j < pkg_res.files.length; j++) {
|
||||
var f = pkg_res.files[j]
|
||||
for (var k = 0; k < f.benchmarks.length; k++) {
|
||||
var b = f.benchmarks[k]
|
||||
if (b.error) continue
|
||||
arrfor(pkg_res.files, function(f) {
|
||||
arrfor(f.benchmarks, function(b) {
|
||||
if (b.error) return
|
||||
|
||||
txt_report += `\n${pkg_res.package}::${b.name}\n`
|
||||
txt_report += ` batch_size: ${b.batch_size} samples: ${b.samples}\n`
|
||||
@@ -573,30 +546,28 @@ Total benchmarks: ${total_benches}
|
||||
txt_report += ` p95: ${format_ns(b.p95_ns)}\n`
|
||||
txt_report += ` p99: ${format_ns(b.p99_ns)}\n`
|
||||
txt_report += ` ops/s: ${format_ops(b.ops_per_sec)}\n`
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
testlib.ensure_dir(report_dir)
|
||||
fd.slurpwrite(`${report_dir}/bench.txt`, stone(new blob(txt_report)))
|
||||
fd.slurpwrite(`${report_dir}/bench.txt`, stone(blob(txt_report)))
|
||||
log.console(`Report written to ${report_dir}/bench.txt`)
|
||||
|
||||
// Generate JSON per package
|
||||
for (var i = 0; i < all_results.length; i++) {
|
||||
var pkg_res = all_results[i]
|
||||
if (pkg_res.total == 0) continue
|
||||
arrfor(all_results, function(pkg_res) {
|
||||
if (pkg_res.total == 0) return
|
||||
|
||||
var pkg_benches = []
|
||||
for (var j = 0; j < pkg_res.files.length; j++) {
|
||||
var f = pkg_res.files[j]
|
||||
for (var k = 0; k < f.benchmarks.length; k++) {
|
||||
pkg_benches.push(f.benchmarks[k])
|
||||
}
|
||||
}
|
||||
arrfor(pkg_res.files, function(f) {
|
||||
arrfor(f.benchmarks, function(benchmark) {
|
||||
push(pkg_benches, benchmark)
|
||||
})
|
||||
})
|
||||
|
||||
var json_path = `${report_dir}/${pkg_res.package.replace(/\//g, '_')}.json`
|
||||
fd.slurpwrite(json_path, stone(new blob(json.encode(pkg_benches))))
|
||||
}
|
||||
var json_path = `${report_dir}/${replace(pkg_res.package, /\//, '_')}.json`
|
||||
fd.slurpwrite(json_path, stone(blob(json.encode(pkg_benches))))
|
||||
})
|
||||
}
|
||||
|
||||
generate_reports()
|
||||
|
||||
@@ -20,14 +20,14 @@ function make_shapes(n) {
|
||||
for (var i = 0; i < n; i++) {
|
||||
var o = { a: i }
|
||||
o[`p${i}`] = i
|
||||
out.push(o)
|
||||
push(out, o)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
function make_packed_array(n) {
|
||||
var a = []
|
||||
for (var i = 0; i < n; i++) a.push(i)
|
||||
for (var i = 0; i < n; i++) push(a, i)
|
||||
return a
|
||||
}
|
||||
|
||||
@@ -203,8 +203,8 @@ return {
|
||||
var x = 0
|
||||
for (var j = 0; j < n; j++) {
|
||||
var a = []
|
||||
for (var i = 0; i < 256; i++) a.push(i)
|
||||
x = (x + a.length) | 0
|
||||
for (var i = 0; i < 256; i++) push(a, i)
|
||||
x = (x + length(a)) | 0
|
||||
}
|
||||
return blackhole(sink, x)
|
||||
},
|
||||
@@ -216,7 +216,7 @@ return {
|
||||
for (var j = 0; j < n; j++) {
|
||||
var s = ""
|
||||
for (var i = 0; i < 16; i++) s = s + "x"
|
||||
x = (x + s.length) | 0
|
||||
x = (x + length(s)) | 0
|
||||
}
|
||||
return blackhole(sink, x)
|
||||
},
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
function mainThread() {
|
||||
var maxDepth = number.max(6, Number(arg[0] || 16));
|
||||
var maxDepth = max(6, Number(arg[0] || 16));
|
||||
|
||||
var stretchDepth = maxDepth + 1;
|
||||
var check = itemCheck(bottomUpTree(stretchDepth));
|
||||
@@ -7,7 +7,7 @@ function mainThread() {
|
||||
|
||||
var longLivedTree = bottomUpTree(maxDepth);
|
||||
|
||||
for (let depth = 4; depth <= maxDepth; depth += 2) {
|
||||
for (var depth = 4; depth <= maxDepth; depth += 2) {
|
||||
var iterations = 1 << maxDepth - depth + 4;
|
||||
work(iterations, depth);
|
||||
}
|
||||
@@ -16,8 +16,8 @@ function mainThread() {
|
||||
}
|
||||
|
||||
function work(iterations, depth) {
|
||||
let check = 0;
|
||||
for (let i = 0; i < iterations; i++)
|
||||
var check = 0;
|
||||
for (var i = 0; i < iterations; i++)
|
||||
check += itemCheck(bottomUpTree(depth));
|
||||
log.console(`${iterations}\t trees of depth ${depth}\t check: ${check}`);
|
||||
}
|
||||
@@ -34,8 +34,8 @@ function itemCheck(node) {
|
||||
|
||||
function bottomUpTree(depth) {
|
||||
return depth > 0
|
||||
? new TreeNode(bottomUpTree(depth - 1), bottomUpTree(depth - 1))
|
||||
: new TreeNode(null, null);
|
||||
? TreeNode(bottomUpTree(depth - 1), bottomUpTree(depth - 1))
|
||||
: TreeNode(null, null);
|
||||
}
|
||||
|
||||
mainThread()
|
||||
|
||||
@@ -2,8 +2,8 @@ var blob = use('blob')
|
||||
var math = use('math/radians')
|
||||
|
||||
function eratosthenes (n) {
|
||||
var sieve = new blob(n, true)
|
||||
var sqrtN = number.whole(math.sqrt(n));
|
||||
var sieve = blob(n, true)
|
||||
var sqrtN = whole(math.sqrt(n));
|
||||
|
||||
for (i = 2; i <= sqrtN; i++)
|
||||
if (sieve.read_logical(i))
|
||||
@@ -17,7 +17,7 @@ var sieve = eratosthenes(10000000);
|
||||
stone(sieve)
|
||||
|
||||
var c = 0
|
||||
for (var i = 0; i < sieve.length; i++)
|
||||
for (var i = 0; i < length(sieve); i++)
|
||||
if (sieve.read_logical(i)) c++
|
||||
|
||||
log.console(c)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
function fannkuch(n) {
|
||||
var perm1 = [n]
|
||||
for (let i = 0; i < n; i++) perm1[i] = i
|
||||
for (var i = 0; i < n; i++) perm1[i] = i
|
||||
var perm = [n]
|
||||
var count = [n]
|
||||
var f = 0, flips = 0, nperm = 0, checksum = 0
|
||||
@@ -18,7 +18,7 @@ function fannkuch(n) {
|
||||
while (k != 0) {
|
||||
i = 0
|
||||
while (2*i < k) {
|
||||
let t = perm[i]; perm[i] = perm[k-i]; perm[k-i] = t
|
||||
var t = perm[i]; perm[i] = perm[k-i]; perm[k-i] = t
|
||||
i += 1
|
||||
}
|
||||
k = perm[0]
|
||||
@@ -34,10 +34,10 @@ function fannkuch(n) {
|
||||
log.console( checksum )
|
||||
return flips
|
||||
}
|
||||
let p0 = perm1[0]
|
||||
var p0 = perm1[0]
|
||||
i = 0
|
||||
while (i < r) {
|
||||
let j = i + 1
|
||||
var j = i + 1
|
||||
perm1[i] = perm1[j]
|
||||
i = j
|
||||
}
|
||||
|
||||
@@ -7,9 +7,9 @@ function fib(n) {
|
||||
|
||||
var now = time.number()
|
||||
var arr = [1,2,3,4,5]
|
||||
for (var i in arr) {
|
||||
arrfor(arr, function(i) {
|
||||
log.console(fib(28))
|
||||
}
|
||||
})
|
||||
|
||||
log.console(`elapsed: ${time.number()-now}`)
|
||||
|
||||
|
||||
@@ -109,12 +109,12 @@ function benchArrayOps() {
|
||||
var pushTime = measureTime(function() {
|
||||
var arr = [];
|
||||
for (var i = 0; i < iterations.medium; i++) {
|
||||
arr.push(i);
|
||||
push(arr, i);
|
||||
}
|
||||
});
|
||||
|
||||
var arr = [];
|
||||
for (var i = 0; i < 10000; i++) arr.push(i);
|
||||
for (var i = 0; i < 10000; i++) push(arr, i);
|
||||
|
||||
var accessTime = measureTime(function() {
|
||||
var sum = 0;
|
||||
@@ -126,7 +126,7 @@ function benchArrayOps() {
|
||||
var iterateTime = measureTime(function() {
|
||||
var sum = 0;
|
||||
for (var j = 0; j < 1000; j++) {
|
||||
for (var i = 0; i < arr.length; i++) {
|
||||
for (var i = 0; i < length(arr); i++) {
|
||||
sum += arr[i];
|
||||
}
|
||||
}
|
||||
@@ -151,13 +151,12 @@ function benchObjectCreation() {
|
||||
});
|
||||
|
||||
function Point(x, y) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
return {x,y}
|
||||
}
|
||||
|
||||
var defructorTime = measureTime(function() {
|
||||
for (var i = 0; i < iterations.medium; i++) {
|
||||
var p = new Point(i, i * 2);
|
||||
var p = Point(i, i * 2);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -199,19 +198,19 @@ function benchStringOps() {
|
||||
|
||||
var strings = [];
|
||||
for (var i = 0; i < 1000; i++) {
|
||||
strings.push("string" + i);
|
||||
push(strings, "string" + i);
|
||||
}
|
||||
|
||||
var joinTime = measureTime(function() {
|
||||
for (var i = 0; i < iterations.complex; i++) {
|
||||
var result = strings.join(",");
|
||||
var result = text(strings, ",");
|
||||
}
|
||||
});
|
||||
|
||||
var splitTime = measureTime(function() {
|
||||
var str = "a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p";
|
||||
for (var i = 0; i < iterations.medium; i++) {
|
||||
var parts = str.split(",");
|
||||
var parts = array(str, ",");
|
||||
}
|
||||
});
|
||||
|
||||
@@ -239,7 +238,7 @@ function benchArithmetic() {
|
||||
var result = 1.5;
|
||||
for (var i = 0; i < iterations.simple; i++) {
|
||||
result = math.sine(result) + math.cosine(i * 0.01);
|
||||
result = math.sqrt(number.abs(result)) + 0.1;
|
||||
result = math.sqrt(abs(result)) + 0.1;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -270,13 +269,13 @@ function benchClosures() {
|
||||
var closureCreateTime = measureTime(function() {
|
||||
var funcs = [];
|
||||
for (var i = 0; i < iterations.medium; i++) {
|
||||
funcs.push(makeAdder(i));
|
||||
push(funcs, makeAdder(i));
|
||||
}
|
||||
});
|
||||
|
||||
var adders = [];
|
||||
for (var i = 0; i < 1000; i++) {
|
||||
adders.push(makeAdder(i));
|
||||
push(adders, makeAdder(i));
|
||||
}
|
||||
|
||||
var closureCallTime = measureTime(function() {
|
||||
|
||||
@@ -8,15 +8,15 @@ var w = h
|
||||
|
||||
log.console(`P4\n${w} ${h}`);
|
||||
|
||||
for (let y = 0; y < h; ++y) {
|
||||
for (var y = 0; y < h; ++y) {
|
||||
// Create a blob for the row - we need w bits
|
||||
var row = new blob(w);
|
||||
var row = blob(w);
|
||||
|
||||
for (let x = 0; x < w; ++x) {
|
||||
for (var x = 0; x < w; ++x) {
|
||||
zr = zi = tr = ti = 0;
|
||||
cr = 2 * x / w - 1.5;
|
||||
ci = 2 * y / h - 1;
|
||||
for (let i = 0; i < iter && (tr + ti <= limit * limit); ++i) {
|
||||
for (var i = 0; i < iter && (tr + ti <= limit * limit); ++i) {
|
||||
zi = 2 * zr * zi + ci;
|
||||
zr = tr - ti + cr;
|
||||
tr = zr * zr;
|
||||
|
||||
@@ -3,17 +3,11 @@ var SOLAR_MASS = 4 * pi * pi;
|
||||
var DAYS_PER_YEAR = 365.24;
|
||||
|
||||
function Body(x, y, z, vx, vy, vz, mass) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.z = z;
|
||||
this.vx = vx;
|
||||
this.vy = vy;
|
||||
this.vz = vz;
|
||||
this.mass = mass;
|
||||
return {x, y, z, vx, vy, vz, mass};
|
||||
}
|
||||
|
||||
function Jupiter() {
|
||||
return new Body(
|
||||
return Body(
|
||||
4.84143144246472090e+00,
|
||||
-1.16032004402742839e+00,
|
||||
-1.03622044471123109e-01,
|
||||
@@ -25,7 +19,7 @@ function Jupiter() {
|
||||
}
|
||||
|
||||
function Saturn() {
|
||||
return new Body(
|
||||
return Body(
|
||||
8.34336671824457987e+00,
|
||||
4.12479856412430479e+00,
|
||||
-4.03523417114321381e-01,
|
||||
@@ -37,7 +31,7 @@ function Saturn() {
|
||||
}
|
||||
|
||||
function Uranus() {
|
||||
return new Body(
|
||||
return Body(
|
||||
1.28943695621391310e+01,
|
||||
-1.51111514016986312e+01,
|
||||
-2.23307578892655734e-01,
|
||||
@@ -49,7 +43,7 @@ function Uranus() {
|
||||
}
|
||||
|
||||
function Neptune() {
|
||||
return new Body(
|
||||
return Body(
|
||||
1.53796971148509165e+01,
|
||||
-2.59193146099879641e+01,
|
||||
1.79258772950371181e-01,
|
||||
@@ -61,7 +55,7 @@ function Neptune() {
|
||||
}
|
||||
|
||||
function Sun() {
|
||||
return new Body(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, SOLAR_MASS);
|
||||
return Body(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, SOLAR_MASS);
|
||||
}
|
||||
|
||||
var bodies = Array(Sun(), Jupiter(), Saturn(), Uranus(), Neptune());
|
||||
@@ -70,7 +64,7 @@ function offsetMomentum() {
|
||||
var px = 0;
|
||||
var py = 0;
|
||||
var pz = 0;
|
||||
var size = bodies.length;
|
||||
var size = length(bodies);
|
||||
for (var i = 0; i < size; i++) {
|
||||
var body = bodies[i];
|
||||
var mass = body.mass;
|
||||
@@ -86,7 +80,7 @@ function offsetMomentum() {
|
||||
}
|
||||
|
||||
function advance(dt) {
|
||||
var size = bodies.length;
|
||||
var size = length(bodies);
|
||||
|
||||
for (var i = 0; i < size; i++) {
|
||||
var bodyi = bodies[i];
|
||||
@@ -127,7 +121,7 @@ function advance(dt) {
|
||||
|
||||
function energy() {
|
||||
var e = 0;
|
||||
var size = bodies.length;
|
||||
var size = length(bodies);
|
||||
|
||||
for (var i = 0; i < size; i++) {
|
||||
var bodyi = bodies[i];
|
||||
|
||||
@@ -9,7 +9,7 @@ var newarr = []
|
||||
var accstr = ""
|
||||
for (var i = 0; i < 10000; i++) {
|
||||
accstr += i;
|
||||
newarr.push(i.toString())
|
||||
newarrpush(i.toString())
|
||||
}
|
||||
// Arrays to store timing results
|
||||
var jsonDecodeTimes = [];
|
||||
@@ -19,34 +19,35 @@ var notaDecodeTimes = [];
|
||||
var notaSizes = [];
|
||||
|
||||
// Run 100 tests
|
||||
for (let i = 0; i < 100; i++) {
|
||||
for (var i = 0; i < 100; i++) {
|
||||
// JSON Decode test
|
||||
let start = os.now();
|
||||
var start = os.now();
|
||||
var jll = json.decode(ll);
|
||||
jsonDecodeTimes.push((os.now() - start) * 1000);
|
||||
jsonDecodeTimespush((os.now() - start) * 1000);
|
||||
|
||||
// JSON Encode test
|
||||
start = os.now();
|
||||
let jsonStr = JSON.stringify(jll);
|
||||
jsonEncodeTimes.push((os.now() - start) * 1000);
|
||||
var jsonStr = JSON.stringify(jll);
|
||||
jsonEncodeTimespush((os.now() - start) * 1000);
|
||||
|
||||
// NOTA Encode test
|
||||
start = os.now();
|
||||
var nll = nota.encode(jll);
|
||||
notaEncodeTimes.push((os.now() - start) * 1000);
|
||||
notaEncodeTimespush((os.now() - start) * 1000);
|
||||
|
||||
// NOTA Decode test
|
||||
start = os.now();
|
||||
var oll = nota.decode(nll);
|
||||
notaDecodeTimes.push((os.now() - start) * 1000);
|
||||
notaDecodeTimespush((os.now() - start) * 1000);
|
||||
}
|
||||
|
||||
// Calculate statistics
|
||||
function getStats(arr) {
|
||||
def avg = arr.reduce((a, b) => a + b) / arr.length;
|
||||
def min = number.min(...arr);
|
||||
def max = number.max(...arr);
|
||||
return { avg, min, max };
|
||||
return {
|
||||
avg: reduce(arr, (a,b) => a+b, 0) / length(arr),
|
||||
min: reduce(arr, min),
|
||||
max: reduce(arr, max)
|
||||
};
|
||||
}
|
||||
|
||||
// Pretty print results
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
const math = require('math/radians');
|
||||
def math = use('math/radians');
|
||||
|
||||
function A(i,j) {
|
||||
return 1/((i+j)*(i+j+1)/2+i+1);
|
||||
}
|
||||
|
||||
function Au(u,v) {
|
||||
for (var i=0; i<u.length; ++i) {
|
||||
for (var i=0; i<length(u); ++i) {
|
||||
var t = 0;
|
||||
for (var j=0; j<u.length; ++j)
|
||||
for (var j=0; j<length(u); ++j)
|
||||
t += A(i,j) * u[j];
|
||||
|
||||
v[i] = t;
|
||||
@@ -15,9 +15,9 @@ function Au(u,v) {
|
||||
}
|
||||
|
||||
function Atu(u,v) {
|
||||
for (var i=0; i<u.length; ++i) {
|
||||
for (var i=0; i<length(u); ++i) {
|
||||
var t = 0;
|
||||
for (var j=0; j<u.length; ++j)
|
||||
for (var j=0; j<length(u); ++j)
|
||||
t += A(j,i) * u[j];
|
||||
|
||||
v[i] = t;
|
||||
|
||||
@@ -14,18 +14,18 @@
|
||||
// Helper to run a function repeatedly and measure total time in seconds.
|
||||
// Returns elapsed time in seconds.
|
||||
function measureTime(fn, iterations) {
|
||||
let t1 = os.now();
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
var t1 = os.now();
|
||||
for (var i = 0; i < iterations; i++) {
|
||||
fn();
|
||||
}
|
||||
let t2 = os.now();
|
||||
var t2 = os.now();
|
||||
return t2 - t1;
|
||||
}
|
||||
|
||||
// We'll define a function that does `encode -> decode` for a given value:
|
||||
function roundTripWota(value) {
|
||||
let encoded = wota.encode(value);
|
||||
let decoded = wota.decode(encoded);
|
||||
var encoded = wota.encode(value);
|
||||
var decoded = wota.decode(encoded);
|
||||
// Not doing a deep compare here, just measuring performance.
|
||||
// (We trust the test suite to verify correctness.)
|
||||
}
|
||||
@@ -63,15 +63,9 @@ def benchmarks = [
|
||||
{
|
||||
name: "Large Array (1k numbers)",
|
||||
// A thousand random numbers
|
||||
data: [ Array.from({length:1000}, (_, i) => i * 0.5) ],
|
||||
data: [ array(1000, i => i *0.5) ],
|
||||
iterations: 1000
|
||||
},
|
||||
{
|
||||
name: "Large Binary Blob (256KB)",
|
||||
// A 256KB ArrayBuffer
|
||||
data: [ new Uint8Array(256 * 1024).buffer ],
|
||||
iterations: 200
|
||||
}
|
||||
];
|
||||
|
||||
// Print a header
|
||||
@@ -79,28 +73,23 @@ log.console("Wota Encode/Decode Benchmark");
|
||||
log.console("===================\n");
|
||||
|
||||
// We'll run each benchmark scenario in turn.
|
||||
for (let bench of benchmarks) {
|
||||
// We'll measure how long it takes to do 'iterations' *for each test value*
|
||||
// in bench.data. The total loop count is `bench.iterations * bench.data.length`.
|
||||
// Then we compute an overall encode+decode throughput (ops/s).
|
||||
let totalIterations = bench.iterations * bench.data.length;
|
||||
arrfor(benchmarks, function(bench) {
|
||||
var totalIterations = bench.iterations * length(bench.data);
|
||||
|
||||
// We'll define a function that does a roundTrip for *each* data item in bench.data
|
||||
// to measure in one loop iteration. Then we multiply by bench.iterations.
|
||||
function runAllData() {
|
||||
for (let val of bench.data) {
|
||||
roundTripWota(val);
|
||||
}
|
||||
arrfor(bench.data, roundTripWota)
|
||||
}
|
||||
|
||||
let elapsedSec = measureTime(runAllData, bench.iterations);
|
||||
let opsPerSec = (totalIterations / elapsedSec).toFixed(1);
|
||||
var elapsedSec = measureTime(runAllData, bench.iterations);
|
||||
var opsPerSec = (totalIterations / elapsedSec).toFixed(1);
|
||||
|
||||
log.console(`${bench.name}:`);
|
||||
log.console(` Iterations: ${bench.iterations} × ${bench.data.length} data items = ${totalIterations}`);
|
||||
log.console(` Iterations: ${bench.iterations} × ${length(bench.data)} data items = ${totalIterations}`);
|
||||
log.console(` Elapsed: ${elapsedSec.toFixed(3)} s`);
|
||||
log.console(` Throughput: ${opsPerSec} encode+decode ops/sec\n`);
|
||||
}
|
||||
})
|
||||
|
||||
// All done
|
||||
log.console("Benchmark completed.\n");
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
//
|
||||
|
||||
// Parse command line arguments
|
||||
if (arg.length != 2) {
|
||||
if (length(arg) != 2) {
|
||||
log.console('Usage: cell benchmark_wota_nota_json.ce <LibraryName> <ScenarioName>');
|
||||
$stop()
|
||||
}
|
||||
@@ -32,7 +32,7 @@ def libraries = [
|
||||
decode: wota.decode,
|
||||
// wota produces an ArrayBuffer. We'll count `buffer.byteLength` as size.
|
||||
getSize(encoded) {
|
||||
return encoded.length;
|
||||
return length(encoded);
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -41,7 +41,7 @@ def libraries = [
|
||||
decode: nota.decode,
|
||||
// nota also produces an ArrayBuffer:
|
||||
getSize(encoded) {
|
||||
return encoded.length;
|
||||
return length(encoded);
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -50,9 +50,8 @@ def libraries = [
|
||||
decode: json.decode,
|
||||
// json produces a JS string. We'll measure its UTF-16 code unit length
|
||||
// as a rough "size". Alternatively, you could convert to UTF-8 for
|
||||
// a more accurate byte size. Here we just use `string.length`.
|
||||
getSize(encodedStr) {
|
||||
return encodedStr.length;
|
||||
return length(encodedStr);
|
||||
}
|
||||
}
|
||||
];
|
||||
@@ -98,7 +97,7 @@ def benchmarks = [
|
||||
},
|
||||
{
|
||||
name: "large_array",
|
||||
data: [ Array.from({length:1000}, (_, i) => i) ],
|
||||
data: [ array(1000, i => i) ],
|
||||
iterations: 1000
|
||||
},
|
||||
];
|
||||
@@ -108,9 +107,9 @@ def benchmarks = [
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
function measureTime(fn) {
|
||||
let start = os.now();
|
||||
var start = os.now();
|
||||
fn();
|
||||
let end = os.now();
|
||||
var end = os.now();
|
||||
return (end - start); // in seconds
|
||||
}
|
||||
|
||||
@@ -128,19 +127,19 @@ function runBenchmarkForLibrary(lib, bench) {
|
||||
|
||||
// Pre-store the encoded results for all items so we can measure decode time
|
||||
// in a separate pass. Also measure total size once.
|
||||
let encodedList = [];
|
||||
let totalSize = 0;
|
||||
var encodedList = [];
|
||||
var totalSize = 0;
|
||||
|
||||
// 1) Measure ENCODING
|
||||
let encodeTime = measureTime(() => {
|
||||
for (let i = 0; i < bench.iterations; i++) {
|
||||
var encodeTime = measureTime(() => {
|
||||
for (var i = 0; i < bench.iterations; i++) {
|
||||
// For each data item, encode it
|
||||
for (let j = 0; j < bench.data.length; j++) {
|
||||
let e = lib.encode(bench.data[j]);
|
||||
for (var j = 0; j < length(bench.data); j++) {
|
||||
var e = lib.encode(bench.data[j]);
|
||||
// store only in the very first iteration, so we can decode them later
|
||||
// but do not store them every iteration or we blow up memory.
|
||||
if (i == 0) {
|
||||
encodedList.push(e);
|
||||
push(encodedList, e);
|
||||
totalSize += lib.getSize(e);
|
||||
}
|
||||
}
|
||||
@@ -148,13 +147,9 @@ function runBenchmarkForLibrary(lib, bench) {
|
||||
});
|
||||
|
||||
// 2) Measure DECODING
|
||||
let decodeTime = measureTime(() => {
|
||||
for (let i = 0; i < bench.iterations; i++) {
|
||||
// decode everything we stored during the first iteration
|
||||
for (let e of encodedList) {
|
||||
let decoded = lib.decode(e);
|
||||
// not verifying correctness here, just measuring speed
|
||||
}
|
||||
var decodeTime = measureTime(() => {
|
||||
for (var i = 0; i < bench.iterations; i++) {
|
||||
arrfor(encodedList, lib.decode)
|
||||
}
|
||||
});
|
||||
|
||||
@@ -166,18 +161,18 @@ function runBenchmarkForLibrary(lib, bench) {
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Find the requested library and scenario
|
||||
var lib = libraries.find(l => l.name == lib_name);
|
||||
var bench = benchmarks.find(b => b.name == scenario_name);
|
||||
var lib = libraries[find(libraries, l => l.name == lib_name)];
|
||||
var bench = benchmarks[find(benchmarks, b => b.name == scenario_name)];
|
||||
|
||||
if (!lib) {
|
||||
log.console('Unknown library:', lib_name);
|
||||
log.console('Available libraries:', libraries.map(l => l.name).join(', '));
|
||||
log.console('Available libraries:', text(array(libraries, l => l.name), ', '));
|
||||
$stop()
|
||||
}
|
||||
|
||||
if (!bench) {
|
||||
log.console('Unknown scenario:', scenario_name);
|
||||
log.console('Available scenarios:', benchmarks.map(b => b.name).join(', '));
|
||||
log.console('Available scenarios:', text(array(benchmarks, b => b.name), ', '));
|
||||
$stop()
|
||||
}
|
||||
|
||||
@@ -185,7 +180,7 @@ if (!bench) {
|
||||
var { encodeTime, decodeTime, totalSize } = runBenchmarkForLibrary(lib, bench);
|
||||
|
||||
// Output json for easy parsing by hyperfine or other tools
|
||||
var totalOps = bench.iterations * bench.data.length;
|
||||
var totalOps = bench.iterations * length(bench.data);
|
||||
var result = {
|
||||
lib: lib_name,
|
||||
scenario: scenario_name,
|
||||
|
||||
41
bootstrap.ce
Normal file
41
bootstrap.ce
Normal file
@@ -0,0 +1,41 @@
|
||||
// bootstrap.ce — regenerate .mach bytecode files consumed by the mach engine
|
||||
// usage: cell bootstrap.ce
|
||||
|
||||
var fd = use("fd")
|
||||
var json = use("json")
|
||||
var tokenize = use("tokenize")
|
||||
var parse = use("parse")
|
||||
var fold = use("fold")
|
||||
|
||||
var files = [
|
||||
{src: "tokenize.cm", name: "tokenize", out: "tokenize.mach"},
|
||||
{src: "parse.cm", name: "parse", out: "parse.mach"},
|
||||
{src: "fold.cm", name: "fold", out: "fold.mach"},
|
||||
{src: "mcode.cm", name: "mcode", out: "mcode.mach"},
|
||||
{src: "internal/bootstrap.cm", name: "bootstrap", out: "internal/bootstrap.mach"}
|
||||
]
|
||||
|
||||
var i = 0
|
||||
var entry = null
|
||||
var src = null
|
||||
var tok_result = null
|
||||
var ast = null
|
||||
var folded = null
|
||||
var ast_json = null
|
||||
var bytecode = null
|
||||
var f = null
|
||||
|
||||
while (i < length(files)) {
|
||||
entry = files[i]
|
||||
src = text(fd.slurp(entry.src))
|
||||
tok_result = tokenize(src, entry.src)
|
||||
ast = parse(tok_result.tokens, src, entry.src, tokenize)
|
||||
folded = fold(ast)
|
||||
ast_json = json.encode(folded)
|
||||
bytecode = mach_compile_ast(entry.name, ast_json)
|
||||
f = fd.open(entry.out, "w")
|
||||
fd.write(f, bytecode)
|
||||
fd.close(f)
|
||||
print(`wrote ${entry.out}`)
|
||||
i = i + 1
|
||||
}
|
||||
50
build.ce
50
build.ce
@@ -1,9 +1,11 @@
|
||||
// cell build [options] - Build dynamic libraries locally for the current machine
|
||||
// cell build [<locator>] - Build dynamic libraries locally for the current machine
|
||||
//
|
||||
// Usage:
|
||||
// cell build Build dynamic libraries for all packages
|
||||
// cell build -p <pkg> Build dynamic library for specific package
|
||||
// cell build Build dynamic libraries for all packages in shop
|
||||
// cell build . Build dynamic library for current directory package
|
||||
// cell build <locator> Build dynamic library for specific package
|
||||
// cell build -t <target> Cross-compile dynamic libraries for target platform
|
||||
// cell build -b <type> Build type: release (default), debug, or minsize
|
||||
|
||||
var build = use('build')
|
||||
var shop = use('internal/shop')
|
||||
@@ -12,25 +14,28 @@ var fd = use('fd')
|
||||
|
||||
var target = null
|
||||
var target_package = null
|
||||
var buildtype = 'debug'
|
||||
var buildtype = 'release'
|
||||
var force_rebuild = false
|
||||
var dry_run = false
|
||||
|
||||
for (var i = 0; i < args.length; i++) {
|
||||
for (var i = 0; i < length(args); i++) {
|
||||
if (args[i] == '-t' || args[i] == '--target') {
|
||||
if (i + 1 < args.length) {
|
||||
if (i + 1 < length(args)) {
|
||||
target = args[++i]
|
||||
} else {
|
||||
log.error('-t requires a target')
|
||||
$stop()
|
||||
}
|
||||
} else if (args[i] == '-p' || args[i] == '--package') {
|
||||
if (i + 1 < args.length) {
|
||||
// Legacy support for -p flag
|
||||
if (i + 1 < length(args)) {
|
||||
target_package = args[++i]
|
||||
} else {
|
||||
log.error('-p requires a package name')
|
||||
$stop()
|
||||
}
|
||||
} else if (args[i] == '-b' || args[i] == '--buildtype') {
|
||||
if (i + 1 < args.length) {
|
||||
if (i + 1 < length(args)) {
|
||||
buildtype = args[++i]
|
||||
if (buildtype != 'release' && buildtype != 'debug' && buildtype != 'minsize') {
|
||||
log.error('Invalid buildtype: ' + buildtype + '. Must be release, debug, or minsize')
|
||||
@@ -40,13 +45,30 @@ for (var i = 0; i < args.length; i++) {
|
||||
log.error('-b requires a buildtype (release, debug, minsize)')
|
||||
$stop()
|
||||
}
|
||||
} else if (args[i] == '--force') {
|
||||
force_rebuild = true
|
||||
} else if (args[i] == '--dry-run') {
|
||||
dry_run = true
|
||||
} else if (args[i] == '--list-targets') {
|
||||
log.console('Available targets:')
|
||||
var targets = build.list_targets()
|
||||
for (var t = 0; t < targets.length; t++) {
|
||||
for (var t = 0; t < length(targets); t++) {
|
||||
log.console(' ' + targets[t])
|
||||
}
|
||||
$stop()
|
||||
} else if (!starts_with(args[i], '-') && !target_package) {
|
||||
// Positional argument - treat as package locator
|
||||
target_package = args[i]
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve local paths to absolute paths
|
||||
if (target_package) {
|
||||
if (target_package == '.' || starts_with(target_package, './') || starts_with(target_package, '../') || fd.is_dir(target_package)) {
|
||||
var resolved = fd.realpath(target_package)
|
||||
if (resolved) {
|
||||
target_package = resolved
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,16 +80,16 @@ if (!target) {
|
||||
|
||||
if (target && !build.has_target(target)) {
|
||||
log.error('Invalid target: ' + target)
|
||||
log.console('Available targets: ' + build.list_targets().join(', '))
|
||||
log.console('Available targets: ' + text(build.list_targets(), ', '))
|
||||
$stop()
|
||||
}
|
||||
|
||||
var packages = shop.list_packages()
|
||||
log.console('Preparing packages...')
|
||||
for (var package of packages) {
|
||||
if (package == 'core') continue
|
||||
arrfor(packages, function(package) {
|
||||
if (package == 'core') return
|
||||
shop.extract(package)
|
||||
}
|
||||
})
|
||||
|
||||
if (target_package) {
|
||||
// Build single package
|
||||
@@ -88,7 +110,7 @@ if (target_package) {
|
||||
|
||||
var success = 0
|
||||
var failed = 0
|
||||
for (var i = 0; i < results.length; i++) {
|
||||
for (var i = 0; i < length(results); i++) {
|
||||
if (results[i].library) {
|
||||
success++
|
||||
} else if (results[i].error) {
|
||||
|
||||
295
build.cm
295
build.cm
@@ -28,15 +28,15 @@ function get_local_dir() {
|
||||
// Replace sigils in a string
|
||||
// Currently supports: $LOCAL -> .cell/local full path
|
||||
function replace_sigils(str) {
|
||||
return str.replaceAll('$LOCAL', get_local_dir())
|
||||
return replace(str, '$LOCAL', get_local_dir())
|
||||
}
|
||||
|
||||
// Replace sigils in an array of flags
|
||||
function replace_sigils_array(flags) {
|
||||
var result = []
|
||||
for (var i = 0; i < flags.length; i++) {
|
||||
result.push(replace_sigils(flags[i]))
|
||||
}
|
||||
arrfor(flags, function(flag) {
|
||||
push(result, replace_sigils(flag))
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ Build.detect_host_target = function() {
|
||||
// ============================================================================
|
||||
|
||||
function content_hash(str) {
|
||||
var bb = stone(new blob(str))
|
||||
var bb = stone(blob(str))
|
||||
return text(crypto.blake2(bb, 32), 'h')
|
||||
}
|
||||
|
||||
@@ -83,14 +83,12 @@ function get_build_dir() {
|
||||
|
||||
function ensure_dir(path) {
|
||||
if (fd.stat(path).isDirectory) return
|
||||
var parts = path.split('/')
|
||||
var current = path.startsWith('/') ? '/' : ''
|
||||
for (var i = 0; i < parts.length; i++) {
|
||||
var parts = array(path, '/')
|
||||
var current = starts_with(path, '/') ? '/' : ''
|
||||
for (var i = 0; i < length(parts); i++) {
|
||||
if (parts[i] == '') continue
|
||||
current += parts[i] + '/'
|
||||
if (!fd.stat(current).isDirectory) {
|
||||
fd.mkdir(current)
|
||||
}
|
||||
if (!fd.stat(current).isDirectory) fd.mkdir(current)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,7 +105,7 @@ Build.compile_file = function(pkg, file, target, buildtype = 'release') {
|
||||
var src_path = pkg_dir + '/' + file
|
||||
|
||||
if (!fd.is_file(src_path)) {
|
||||
throw new Error('Source file not found: ' + src_path)
|
||||
throw Error('Source file not found: ' + src_path)
|
||||
}
|
||||
|
||||
// Get flags (with sigil replacement)
|
||||
@@ -123,33 +121,32 @@ Build.compile_file = function(pkg, file, target, buildtype = 'release') {
|
||||
|
||||
// Add buildtype-specific flags
|
||||
if (buildtype == 'release') {
|
||||
cmd_parts.push('-O3', '-DNDEBUG')
|
||||
cmd_parts = array(cmd_parts, ['-O3', '-DNDEBUG'])
|
||||
} else if (buildtype == 'debug') {
|
||||
cmd_parts.push('-O2', '-g')
|
||||
cmd_parts = array(cmd_parts, ['-O2', '-g'])
|
||||
} else if (buildtype == 'minsize') {
|
||||
cmd_parts.push('-Os', '-DNDEBUG')
|
||||
cmd_parts = array(cmd_parts, ['-Os', '-DNDEBUG'])
|
||||
}
|
||||
|
||||
cmd_parts.push('-DCELL_USE_NAME=' + sym_name)
|
||||
cmd_parts.push('-I"' + pkg_dir + '"')
|
||||
push(cmd_parts, '-DCELL_USE_NAME=' + sym_name)
|
||||
push(cmd_parts, '-I"' + pkg_dir + '"')
|
||||
|
||||
// Add package CFLAGS (resolve relative -I paths)
|
||||
for (var i = 0; i < cflags.length; i++) {
|
||||
var flag = cflags[i]
|
||||
if (flag.startsWith('-I') && !flag.startsWith('-I/')) {
|
||||
flag = '-I"' + pkg_dir + '/' + flag.substring(2) + '"'
|
||||
arrfor(cflags, function(flag) {
|
||||
if (starts_with(flag, '-I') && !starts_with(flag, '-I/')) {
|
||||
flag = '-I"' + pkg_dir + '/' + text(flag, 2) + '"'
|
||||
}
|
||||
cmd_parts.push(flag)
|
||||
}
|
||||
push(cmd_parts, flag)
|
||||
})
|
||||
|
||||
// Add target CFLAGS
|
||||
for (var i = 0; i < target_cflags.length; i++) {
|
||||
cmd_parts.push(target_cflags[i])
|
||||
}
|
||||
arrfor(target_cflags, function(flag) {
|
||||
push(cmd_parts, flag)
|
||||
})
|
||||
|
||||
cmd_parts.push('"' + src_path + '"')
|
||||
push(cmd_parts, '"' + src_path + '"')
|
||||
|
||||
var cmd_str = cmd_parts.join(' ')
|
||||
var cmd_str = text(cmd_parts, ' ')
|
||||
|
||||
// Content hash: command + file content
|
||||
var file_content = fd.slurp(src_path)
|
||||
@@ -170,7 +167,7 @@ Build.compile_file = function(pkg, file, target, buildtype = 'release') {
|
||||
log.console('Compiling ' + file)
|
||||
var ret = os.system(full_cmd)
|
||||
if (ret != 0) {
|
||||
throw new Error('Compilation failed: ' + file)
|
||||
throw Error('Compilation failed: ' + file)
|
||||
}
|
||||
|
||||
return obj_path
|
||||
@@ -182,10 +179,10 @@ Build.build_package = function(pkg, target = Build.detect_host_target(), exclude
|
||||
var c_files = pkg_tools.get_c_files(pkg, target, exclude_main)
|
||||
var objects = []
|
||||
|
||||
for (var i = 0; i < c_files.length; i++) {
|
||||
var obj = Build.compile_file(pkg, c_files[i], target, buildtype)
|
||||
objects.push(obj)
|
||||
}
|
||||
arrfor(c_files, function(file) {
|
||||
var obj = Build.compile_file(pkg, file, target, buildtype)
|
||||
push(objects, obj)
|
||||
})
|
||||
|
||||
return objects
|
||||
}
|
||||
@@ -193,24 +190,51 @@ Build.build_package = function(pkg, target = Build.detect_host_target(), exclude
|
||||
// ============================================================================
|
||||
// Dynamic library building
|
||||
// ============================================================================
|
||||
|
||||
// Compute link key from all inputs that affect the dylib output
|
||||
function compute_link_key(objects, ldflags, target_ldflags, target, cc) {
|
||||
// Sort objects for deterministic hash
|
||||
var sorted_objects = sort(objects)
|
||||
|
||||
// Build a string representing all link inputs
|
||||
var parts = []
|
||||
push(parts, 'target:' + target)
|
||||
push(parts, 'cc:' + cc)
|
||||
arrfor(sorted_objects, function(obj) {
|
||||
// Object paths are content-addressed, so the path itself is the hash
|
||||
push(parts, 'obj:' + obj)
|
||||
})
|
||||
arrfor(ldflags, function(flag) {
|
||||
push(parts, 'ldflag:' + flag)
|
||||
})
|
||||
arrfor(target_ldflags, function(flag) {
|
||||
push(parts, 'target_ldflag:' + flag)
|
||||
})
|
||||
|
||||
return content_hash(text(parts, '\n'))
|
||||
}
|
||||
|
||||
// Build a dynamic library for a package
|
||||
// Output goes to .cell/lib/<package_name>.<ext>
|
||||
// Dynamic libraries do NOT link against core; undefined symbols are resolved at dlopen time
|
||||
// Uses content-addressed store + symlink for caching
|
||||
Build.build_dynamic = function(pkg, target = Build.detect_host_target(), buildtype = 'release') {
|
||||
var objects = Build.build_package(pkg, target, true, buildtype) // exclude main.c
|
||||
|
||||
if (objects.length == 0) {
|
||||
|
||||
if (length(objects) == 0) {
|
||||
log.console('No C files in ' + pkg)
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
var lib_dir = shop.get_lib_dir()
|
||||
var store_dir = lib_dir + '/store'
|
||||
ensure_dir(lib_dir)
|
||||
|
||||
ensure_dir(store_dir)
|
||||
|
||||
var lib_name = shop.lib_name_for_package(pkg)
|
||||
var dylib_ext = toolchains[target].system == 'windows' ? '.dll' : (toolchains[target].system == 'darwin' ? '.dylib' : '.so')
|
||||
var lib_path = lib_dir + '/' + lib_name + dylib_ext
|
||||
|
||||
var stable_path = lib_dir + '/' + lib_name + dylib_ext
|
||||
|
||||
// Get link flags (with sigil replacement)
|
||||
var ldflags = replace_sigils_array(pkg_tools.get_flags(pkg, 'LDFLAGS', target))
|
||||
var target_ldflags = toolchains[target].c_link_args || []
|
||||
@@ -218,65 +242,92 @@ Build.build_dynamic = function(pkg, target = Build.detect_host_target(), buildty
|
||||
var pkg_dir = shop.get_package_dir(pkg)
|
||||
var local_dir = get_local_dir()
|
||||
var tc = toolchains[target]
|
||||
|
||||
|
||||
// Resolve relative -L paths in ldflags for hash computation
|
||||
var resolved_ldflags = []
|
||||
arrfor(ldflags, function(flag) {
|
||||
if (starts_with(flag, '-L') && !starts_with(flag, '-L/')) {
|
||||
flag = '-L"' + pkg_dir + '/' + text(flag, 2) + '"'
|
||||
}
|
||||
push(resolved_ldflags, flag)
|
||||
})
|
||||
|
||||
// Compute link key
|
||||
var link_key = compute_link_key(objects, resolved_ldflags, target_ldflags, target, cc)
|
||||
var store_path = store_dir + '/' + lib_name + '-' + link_key + dylib_ext
|
||||
|
||||
// Check if already linked in store
|
||||
if (fd.is_file(store_path)) {
|
||||
// Ensure symlink points to the store file
|
||||
if (fd.is_link(stable_path)) {
|
||||
var current_target = fd.readlink(stable_path)
|
||||
if (current_target == store_path) {
|
||||
// Already up to date
|
||||
return stable_path
|
||||
}
|
||||
fd.unlink(stable_path)
|
||||
} else if (fd.is_file(stable_path)) {
|
||||
fd.unlink(stable_path)
|
||||
}
|
||||
fd.symlink(store_path, stable_path)
|
||||
return stable_path
|
||||
}
|
||||
|
||||
// Build link command
|
||||
var cmd_parts = [cc, '-shared', '-fPIC']
|
||||
|
||||
|
||||
// Platform-specific flags for undefined symbols (resolved at dlopen) and size optimization
|
||||
if (tc.system == 'darwin') {
|
||||
// Allow undefined symbols - they will be resolved when dlopen'd into the main executable
|
||||
cmd_parts.push('-undefined', 'dynamic_lookup')
|
||||
// Dead-strip unused code
|
||||
cmd_parts.push('-Wl,-dead_strip')
|
||||
// rpath for .cell/local libraries
|
||||
cmd_parts.push('-Wl,-rpath,@loader_path/../local')
|
||||
cmd_parts.push('-Wl,-rpath,' + local_dir)
|
||||
cmd_parts = array(cmd_parts, [
|
||||
'-undefined', 'dynamic_lookup',
|
||||
'-Wl,-dead_strip',
|
||||
'-Wl,-install_name,' + stable_path,
|
||||
'-Wl,-rpath,@loader_path/../local',
|
||||
'-Wl,-rpath,' + local_dir
|
||||
])
|
||||
} else if (tc.system == 'linux') {
|
||||
// Allow undefined symbols at link time
|
||||
cmd_parts.push('-Wl,--allow-shlib-undefined')
|
||||
// Garbage collect unused sections
|
||||
cmd_parts.push('-Wl,--gc-sections')
|
||||
// rpath for .cell/local libraries
|
||||
cmd_parts.push('-Wl,-rpath,$ORIGIN/../local')
|
||||
cmd_parts.push('-Wl,-rpath,' + local_dir)
|
||||
cmd_parts = array(cmd_parts, [
|
||||
'-Wl,--allow-shlib-undefined',
|
||||
'-Wl,--gc-sections',
|
||||
'-Wl,-rpath,$ORIGIN/../local',
|
||||
'-Wl,-rpath,' + local_dir
|
||||
])
|
||||
} else if (tc.system == 'windows') {
|
||||
// Windows DLLs: use --allow-shlib-undefined for mingw
|
||||
cmd_parts.push('-Wl,--allow-shlib-undefined')
|
||||
push(cmd_parts, '-Wl,--allow-shlib-undefined')
|
||||
}
|
||||
|
||||
|
||||
// Add .cell/local to library search path
|
||||
cmd_parts.push('-L"' + local_dir + '"')
|
||||
|
||||
for (var i = 0; i < objects.length; i++) {
|
||||
cmd_parts.push('"' + objects[i] + '"')
|
||||
}
|
||||
|
||||
push(cmd_parts, '-L"' + local_dir + '"')
|
||||
|
||||
arrfor(objects, function(obj) {
|
||||
push(cmd_parts, '"' + obj + '"')
|
||||
})
|
||||
|
||||
// Do NOT link against core library - symbols resolved at dlopen time
|
||||
|
||||
// Add LDFLAGS (resolve relative -L paths)
|
||||
for (var i = 0; i < ldflags.length; i++) {
|
||||
var flag = ldflags[i]
|
||||
if (flag.startsWith('-L') && !flag.startsWith('-L/')) {
|
||||
flag = '-L"' + pkg_dir + '/' + flag.substring(2) + '"'
|
||||
}
|
||||
cmd_parts.push(flag)
|
||||
}
|
||||
|
||||
for (var i = 0; i < target_ldflags.length; i++) {
|
||||
cmd_parts.push(target_ldflags[i])
|
||||
}
|
||||
|
||||
cmd_parts.push('-o', '"' + lib_path + '"')
|
||||
|
||||
var cmd_str = cmd_parts.join(' ')
|
||||
|
||||
log.console('Linking ' + lib_path)
|
||||
cmd_parts = array(cmd_parts, resolved_ldflags)
|
||||
cmd_parts = array(cmd_parts, target_ldflags)
|
||||
|
||||
push(cmd_parts, '-o')
|
||||
push(cmd_parts, '"' + store_path + '"')
|
||||
|
||||
var cmd_str = text(cmd_parts, ' ')
|
||||
|
||||
log.console('Linking ' + lib_name + dylib_ext)
|
||||
var ret = os.system(cmd_str)
|
||||
if (ret != 0) {
|
||||
throw new Error('Linking failed: ' + pkg)
|
||||
throw Error('Linking failed: ' + pkg)
|
||||
}
|
||||
|
||||
return lib_path
|
||||
|
||||
// Update symlink to point to the new store file
|
||||
if (fd.is_link(stable_path)) {
|
||||
fd.unlink(stable_path)
|
||||
} else if (fd.is_file(stable_path)) {
|
||||
fd.unlink(stable_path)
|
||||
}
|
||||
fd.symlink(store_path, stable_path)
|
||||
|
||||
return stable_path
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -292,38 +343,36 @@ Build.build_static = function(packages, target = Build.detect_host_target(), out
|
||||
var seen_flags = {}
|
||||
|
||||
// Compile all packages
|
||||
for (var i = 0; i < packages.length; i++) {
|
||||
var pkg = packages[i]
|
||||
arrfor(packages, function(pkg) {
|
||||
var is_core = (pkg == 'core')
|
||||
|
||||
// For core, include main.c; for others, exclude it
|
||||
var objects = Build.build_package(pkg, target, !is_core, buildtype)
|
||||
|
||||
for (var j = 0; j < objects.length; j++) {
|
||||
all_objects.push(objects[j])
|
||||
}
|
||||
arrfor(objects, function(obj) {
|
||||
push(all_objects, obj)
|
||||
})
|
||||
|
||||
// Collect LDFLAGS (with sigil replacement)
|
||||
var ldflags = replace_sigils_array(pkg_tools.get_flags(pkg, 'LDFLAGS', target))
|
||||
var pkg_dir = shop.get_package_dir(pkg)
|
||||
|
||||
// Deduplicate based on the entire LDFLAGS string for this package
|
||||
var ldflags_key = pkg + ':' + ldflags.join(' ')
|
||||
var ldflags_key = pkg + ':' + text(ldflags, ' ')
|
||||
if (!seen_flags[ldflags_key]) {
|
||||
seen_flags[ldflags_key] = true
|
||||
for (var j = 0; j < ldflags.length; j++) {
|
||||
var flag = ldflags[j]
|
||||
arrfor(ldflags, function(flag) {
|
||||
// Resolve relative -L paths
|
||||
if (flag.startsWith('-L') && !flag.startsWith('-L/')) {
|
||||
flag = '-L"' + pkg_dir + '/' + flag.substring(2) + '"'
|
||||
if (starts_with(flag, '-L') && !starts_with(flag, '-L/')) {
|
||||
flag = '-L"' + pkg_dir + '/' + text(flag, 2) + '"'
|
||||
}
|
||||
all_ldflags.push(flag)
|
||||
}
|
||||
push(all_ldflags, flag)
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if (all_objects.length == 0) {
|
||||
throw new Error('No object files to link')
|
||||
if (length(all_objects) == 0) {
|
||||
throw Error('No object files to link')
|
||||
}
|
||||
|
||||
// Link
|
||||
@@ -331,32 +380,32 @@ Build.build_static = function(packages, target = Build.detect_host_target(), out
|
||||
var target_ldflags = toolchains[target].c_link_args || []
|
||||
var exe_ext = toolchains[target].system == 'windows' ? '.exe' : ''
|
||||
|
||||
if (!output.endsWith(exe_ext) && exe_ext) {
|
||||
if (!ends_with(output, exe_ext) && exe_ext) {
|
||||
output = output + exe_ext
|
||||
}
|
||||
|
||||
var cmd_parts = [cc]
|
||||
|
||||
for (var i = 0; i < all_objects.length; i++) {
|
||||
cmd_parts.push('"' + all_objects[i] + '"')
|
||||
}
|
||||
arrfor(all_objects, function(obj) {
|
||||
push(cmd_parts, '"' + obj + '"')
|
||||
})
|
||||
|
||||
for (var i = 0; i < all_ldflags.length; i++) {
|
||||
cmd_parts.push(all_ldflags[i])
|
||||
}
|
||||
arrfor(all_ldflags, function(flag) {
|
||||
push(cmd_parts, flag)
|
||||
})
|
||||
|
||||
for (var i = 0; i < target_ldflags.length; i++) {
|
||||
cmd_parts.push(target_ldflags[i])
|
||||
}
|
||||
arrfor(target_ldflags, function(flag) {
|
||||
push(cmd_parts, flag)
|
||||
})
|
||||
|
||||
cmd_parts.push('-o', '"' + output + '"')
|
||||
push(cmd_parts, '-o', '"' + output + '"')
|
||||
|
||||
var cmd_str = cmd_parts.join(' ')
|
||||
var cmd_str = text(cmd_parts, ' ')
|
||||
|
||||
log.console('Linking ' + output)
|
||||
var ret = os.system(cmd_str)
|
||||
if (ret != 0) {
|
||||
throw new Error('Linking failed with command: ' + cmd_str)
|
||||
throw Error('Linking failed with command: ' + cmd_str)
|
||||
}
|
||||
|
||||
log.console('Built ' + output)
|
||||
@@ -375,30 +424,30 @@ Build.build_all_dynamic = function(target, buildtype = 'release') {
|
||||
var results = []
|
||||
|
||||
// Build core first
|
||||
if (packages.indexOf('core') >= 0) {
|
||||
if (find(packages, 'core') != null) {
|
||||
try {
|
||||
var lib = Build.build_dynamic('core', target, buildtype)
|
||||
results.push({ package: 'core', library: lib })
|
||||
push(results, { package: 'core', library: lib })
|
||||
} catch (e) {
|
||||
log.error('Failed to build core: ' + text(e))
|
||||
results.push({ package: 'core', error: e })
|
||||
push(results, { package: 'core', error: e })
|
||||
}
|
||||
}
|
||||
|
||||
// Build other packages
|
||||
for (var i = 0; i < packages.length; i++) {
|
||||
var pkg = packages[i]
|
||||
if (pkg == 'core') continue
|
||||
arrfor(packages, function(pkg) {
|
||||
if (pkg == 'core') return
|
||||
|
||||
try {
|
||||
var lib = Build.build_dynamic(pkg, target, buildtype)
|
||||
results.push({ package: pkg, library: lib })
|
||||
push(results, { package: pkg, library: lib })
|
||||
} catch (e) {
|
||||
log.error('Failed to build ' + pkg + ': ')
|
||||
log.error(e)
|
||||
results.push({ package: pkg, error: e })
|
||||
log.console(e.message)
|
||||
log.console(e.stack)
|
||||
push(results, { package: pkg, error: e })
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
189
cellfs.cm
189
cellfs.cm
@@ -17,30 +17,7 @@ var writepath = "."
|
||||
function normalize_path(path) {
|
||||
if (!path) return ""
|
||||
// Remove leading/trailing slashes and normalize
|
||||
return path.replace(/^\/+|\/+$/g, "")
|
||||
}
|
||||
|
||||
// Helper to get directory from path
|
||||
function dirname(path) {
|
||||
var idx = path.lastIndexOf("/")
|
||||
if (idx == -1) return ""
|
||||
return path.substring(0, idx)
|
||||
}
|
||||
|
||||
// Helper to get basename from path
|
||||
function basename(path) {
|
||||
var idx = path.lastIndexOf("/")
|
||||
if (idx == -1) return path
|
||||
return path.substring(idx + 1)
|
||||
}
|
||||
|
||||
// Helper to join paths
|
||||
function join_paths(base, rel) {
|
||||
base = base.replace(/\/+$/, "")
|
||||
rel = rel.replace(/^\/+/, "")
|
||||
if (!base) return rel
|
||||
if (!rel) return base
|
||||
return base + "/" + rel
|
||||
return replace(path, /^\/+|\/+$/, "")
|
||||
}
|
||||
|
||||
// Check if a file exists in a specific mount
|
||||
@@ -59,7 +36,7 @@ function mount_exists(mount, path) {
|
||||
return false
|
||||
}
|
||||
} else { // fs
|
||||
var full_path = join_paths(mount.source, path)
|
||||
var full_path = fd.join_paths(mount.source, path)
|
||||
try {
|
||||
var st = fd.stat(full_path)
|
||||
return st.isFile || st.isDirectory
|
||||
@@ -86,7 +63,7 @@ function is_directory(path) {
|
||||
return false;
|
||||
}
|
||||
} else { // fs
|
||||
var full_path = join_paths(mount.source, path)
|
||||
var full_path = fd.join_paths(mount.source, path)
|
||||
try {
|
||||
var st = fd.stat(full_path)
|
||||
return st.isDirectory
|
||||
@@ -102,44 +79,50 @@ function resolve(path, must_exist) {
|
||||
path = normalize_path(path)
|
||||
|
||||
// Check for named mount
|
||||
if (path.startsWith("@")) {
|
||||
var idx = path.indexOf("/")
|
||||
if (starts_with(path, "@")) {
|
||||
var idx = search(path, "/")
|
||||
var mount_name = ""
|
||||
var rel_path = ""
|
||||
|
||||
if (idx == -1) {
|
||||
mount_name = path.substring(1)
|
||||
if (idx == null) {
|
||||
mount_name = text(path, 1)
|
||||
rel_path = ""
|
||||
} else {
|
||||
mount_name = path.substring(1, idx)
|
||||
rel_path = path.substring(idx + 1)
|
||||
mount_name = text(path, 1, idx)
|
||||
rel_path = text(path, idx + 1)
|
||||
}
|
||||
|
||||
// Find named mount
|
||||
var mount = null
|
||||
for (var m of mounts) {
|
||||
arrfor(mounts, function(m) {
|
||||
if (m.name == mount_name) {
|
||||
mount = m
|
||||
break
|
||||
return true
|
||||
}
|
||||
}
|
||||
}, false, true)
|
||||
|
||||
if (!mount) {
|
||||
throw new Error("Unknown mount point: @" + mount_name)
|
||||
throw Error("Unknown mount point: @" + mount_name)
|
||||
}
|
||||
|
||||
return { mount: mount, path: rel_path }
|
||||
}
|
||||
|
||||
// Search path
|
||||
for (var mount of mounts) {
|
||||
var found_mount = null
|
||||
arrfor(mounts, function(mount) {
|
||||
if (mount_exists(mount, path)) {
|
||||
return { mount: mount, path: path }
|
||||
found_mount = { mount: mount, path: path }
|
||||
return true
|
||||
}
|
||||
}, false, true)
|
||||
|
||||
if (found_mount) {
|
||||
return found_mount
|
||||
}
|
||||
|
||||
if (must_exist) {
|
||||
throw new Error("File not found in any mount: " + path)
|
||||
throw Error("File not found in any mount: " + path)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,8 +157,8 @@ function mount(source, name) {
|
||||
mount_info.zip_blob = blob // keep blob alive
|
||||
} else {
|
||||
var zip = miniz.read(blob)
|
||||
if (!zip || typeof zip.count != 'function') {
|
||||
throw new Error("Invalid archive file (not zip or qop): " + source)
|
||||
if (!is_object(zip) || !is_function(zip.count)) {
|
||||
throw Error("Invalid archive file (not zip or qop): " + source)
|
||||
}
|
||||
|
||||
mount_info.type = 'zip'
|
||||
@@ -183,36 +166,32 @@ function mount(source, name) {
|
||||
mount_info.zip_blob = blob // keep blob alive
|
||||
}
|
||||
} else {
|
||||
throw new Error("Unsupported mount source type: " + source)
|
||||
throw Error("Unsupported mount source type: " + source)
|
||||
}
|
||||
|
||||
mounts.push(mount_info)
|
||||
push(mounts, mount_info)
|
||||
}
|
||||
|
||||
// Unmount
|
||||
function unmount(name_or_source) {
|
||||
for (var i = 0; i < mounts.length; i++) {
|
||||
if (mounts[i].name == name_or_source || mounts[i].source == name_or_source) {
|
||||
mounts.splice(i, 1)
|
||||
return
|
||||
}
|
||||
}
|
||||
throw new Error("Mount not found: " + name_or_source)
|
||||
mounts = filter(mounts, function(mount) {
|
||||
return mount.name != name_or_source && mount.source != name_or_source
|
||||
})
|
||||
}
|
||||
|
||||
// Read file
|
||||
function slurp(path) {
|
||||
var res = resolve(path, true)
|
||||
if (!res) throw new Error("File not found: " + path)
|
||||
if (!res) throw Error("File not found: " + path)
|
||||
|
||||
if (res.mount.type == 'zip') {
|
||||
return res.mount.handle.slurp(res.path)
|
||||
} else if (res.mount.type == 'qop') {
|
||||
var data = res.mount.handle.read(res.path)
|
||||
if (!data) throw new Error("File not found in qop: " + path)
|
||||
if (!data) throw Error("File not found in qop: " + path)
|
||||
return data
|
||||
} else {
|
||||
var full_path = join_paths(res.mount.source, res.path)
|
||||
var full_path = fd.join_paths(res.mount.source, res.path)
|
||||
return fd.slurp(full_path)
|
||||
}
|
||||
}
|
||||
@@ -229,7 +208,7 @@ function slurpwrite(path, data) {
|
||||
// Check existence
|
||||
function exists(path) {
|
||||
var res = resolve(path, false)
|
||||
if (path.startsWith("@")) {
|
||||
if (starts_with(path, "@")) {
|
||||
return mount_exists(res.mount, res.path)
|
||||
}
|
||||
return res != null
|
||||
@@ -238,7 +217,7 @@ function exists(path) {
|
||||
// Stat
|
||||
function stat(path) {
|
||||
var res = resolve(path, true)
|
||||
if (!res) throw new Error("File not found: " + path)
|
||||
if (!res) throw Error("File not found: " + path)
|
||||
|
||||
if (res.mount.type == 'zip') {
|
||||
var mod = res.mount.handle.mod(res.path)
|
||||
@@ -249,14 +228,14 @@ function stat(path) {
|
||||
}
|
||||
} else if (res.mount.type == 'qop') {
|
||||
var s = res.mount.handle.stat(res.path)
|
||||
if (!s) throw new Error("File not found in qop: " + path)
|
||||
if (!s) throw Error("File not found in qop: " + path)
|
||||
return {
|
||||
filesize: s.size,
|
||||
modtime: s.modtime,
|
||||
isDirectory: s.isDirectory
|
||||
}
|
||||
} else {
|
||||
var full_path = join_paths(res.mount.source, res.path)
|
||||
var full_path = fd.join_paths(res.mount.source, res.path)
|
||||
var s = fd.stat(full_path)
|
||||
return {
|
||||
filesize: s.size,
|
||||
@@ -268,7 +247,7 @@ function stat(path) {
|
||||
|
||||
// Get search paths
|
||||
function searchpath() {
|
||||
return mounts.slice()
|
||||
return array(mounts)
|
||||
}
|
||||
|
||||
// Mount a package using the shop system
|
||||
@@ -282,7 +261,7 @@ function mount_package(name) {
|
||||
var dir = shop.get_package_dir(name)
|
||||
|
||||
if (!dir) {
|
||||
throw new Error("Package not found: " + name)
|
||||
throw Error("Package not found: " + name)
|
||||
}
|
||||
|
||||
mount(dir, name)
|
||||
@@ -296,16 +275,16 @@ function match(str, pattern) {
|
||||
|
||||
function rm(path) {
|
||||
var res = resolve(path, true)
|
||||
if (res.mount.type != 'fs') throw new Error("Cannot delete from non-fs mount")
|
||||
if (res.mount.type != 'fs') throw Error("Cannot delete from non-fs mount")
|
||||
|
||||
var full_path = join_paths(res.mount.source, res.path)
|
||||
var full_path = fd.join_paths(res.mount.source, res.path)
|
||||
var st = fd.stat(full_path)
|
||||
if (st.isDirectory) fd.rmdir(full_path)
|
||||
else fd.unlink(full_path)
|
||||
}
|
||||
|
||||
function mkdir(path) {
|
||||
var full = join_paths(writepath, path)
|
||||
var full = fd.join_paths(writepath, path)
|
||||
fd.mkdir(full)
|
||||
}
|
||||
|
||||
@@ -324,7 +303,7 @@ function prefdir(org, app) {
|
||||
function realdir(path) {
|
||||
var res = resolve(path, false)
|
||||
if (!res) return null
|
||||
return join_paths(res.mount.source, res.path)
|
||||
return fd.join_paths(res.mount.source, res.path)
|
||||
}
|
||||
|
||||
function enumerate(path, recurse) {
|
||||
@@ -337,21 +316,21 @@ function enumerate(path, recurse) {
|
||||
var list = fd.readdir(curr_full)
|
||||
if (!list) return
|
||||
|
||||
for (var item of list) {
|
||||
arrfor(list, function(item) {
|
||||
var item_rel = rel_prefix ? rel_prefix + "/" + item : item
|
||||
results.push(item_rel)
|
||||
push(results, item_rel)
|
||||
|
||||
if (recurse) {
|
||||
var st = fd.stat(join_paths(curr_full, item))
|
||||
var st = fd.stat(fd.join_paths(curr_full, item))
|
||||
if (st.isDirectory) {
|
||||
visit(join_paths(curr_full, item), item_rel)
|
||||
visit(fd.join_paths(curr_full, item), item_rel)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (res.mount.type == 'fs') {
|
||||
var full = join_paths(res.mount.source, res.path)
|
||||
var full = fd.join_paths(res.mount.source, res.path)
|
||||
var st = fd.stat(full)
|
||||
if (st && st.isDirectory) {
|
||||
visit(full, "")
|
||||
@@ -359,29 +338,29 @@ function enumerate(path, recurse) {
|
||||
} else if (res.mount.type == 'qop') {
|
||||
var all = res.mount.handle.list()
|
||||
var prefix = res.path ? res.path + "/" : ""
|
||||
var prefix_len = prefix.length
|
||||
var prefix_len = length(prefix)
|
||||
|
||||
// Use a set to avoid duplicates if we are simulating directories
|
||||
var seen = {}
|
||||
|
||||
for (var p of all) {
|
||||
if (p.startsWith(prefix)) {
|
||||
var rel = p.substring(prefix_len)
|
||||
if (rel.length == 0) continue
|
||||
|
||||
arrfor(all, function(p) {
|
||||
if (starts_with(p, prefix)) {
|
||||
var rel = text(p, prefix_len)
|
||||
if (length(rel) == 0) return
|
||||
|
||||
if (!recurse) {
|
||||
var slash = rel.indexOf('/')
|
||||
if (slash != -1) {
|
||||
rel = rel.substring(0, slash)
|
||||
var slash = search(rel, '/')
|
||||
if (slash != null) {
|
||||
rel = text(rel, 0, slash)
|
||||
}
|
||||
}
|
||||
|
||||
if (!seen[rel]) {
|
||||
seen[rel] = true
|
||||
results.push(rel)
|
||||
push(results, rel)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return results
|
||||
@@ -393,17 +372,25 @@ function globfs(globs, dir) {
|
||||
var results = []
|
||||
|
||||
function check_neg(path) {
|
||||
for (var g of globs) {
|
||||
if (g.startsWith("!") && wildstar.match(g.substring(1), path, wildstar.WM_WILDSTAR)) return true;
|
||||
}
|
||||
return false;
|
||||
var result = false
|
||||
arrfor(globs, function(g) {
|
||||
if (starts_with(g, "!") && wildstar.match(text(g, 1), path, wildstar.WM_WILDSTAR)) {
|
||||
result = true
|
||||
return true
|
||||
}
|
||||
}, false, true)
|
||||
return result
|
||||
}
|
||||
|
||||
function check_pos(path) {
|
||||
for (var g of globs) {
|
||||
if (!g.startsWith("!") && wildstar.match(g, path, wildstar.WM_WILDSTAR)) return true;
|
||||
}
|
||||
return false;
|
||||
var result = false
|
||||
arrfor(globs, function(g) {
|
||||
if (!starts_with(g, "!") && wildstar.match(g, path, wildstar.WM_WILDSTAR)) {
|
||||
result = true
|
||||
return true
|
||||
}
|
||||
}, false, true)
|
||||
return result
|
||||
}
|
||||
|
||||
function visit(curr_full, rel_prefix) {
|
||||
@@ -412,10 +399,10 @@ function globfs(globs, dir) {
|
||||
var list = fd.readdir(curr_full)
|
||||
if (!list) return
|
||||
|
||||
for (var item of list) {
|
||||
arrfor(list, function(item) {
|
||||
var item_rel = rel_prefix ? rel_prefix + "/" + item : item
|
||||
|
||||
var child_full = join_paths(curr_full, item)
|
||||
var child_full = fd.join_paths(curr_full, item)
|
||||
var st = fd.stat(child_full)
|
||||
|
||||
if (st.isDirectory) {
|
||||
@@ -424,14 +411,14 @@ function globfs(globs, dir) {
|
||||
}
|
||||
} else {
|
||||
if (!check_neg(item_rel) && check_pos(item_rel)) {
|
||||
results.push(item_rel)
|
||||
push(results, item_rel)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (res.mount.type == 'fs') {
|
||||
var full = join_paths(res.mount.source, res.path)
|
||||
var full = fd.join_paths(res.mount.source, res.path)
|
||||
var st = fd.stat(full)
|
||||
if (st && st.isDirectory) {
|
||||
visit(full, "")
|
||||
@@ -439,18 +426,18 @@ function globfs(globs, dir) {
|
||||
} else if (res.mount.type == 'qop') {
|
||||
var all = res.mount.handle.list()
|
||||
var prefix = res.path ? res.path + "/" : ""
|
||||
var prefix_len = prefix.length
|
||||
var prefix_len = length(prefix)
|
||||
|
||||
for (var p of all) {
|
||||
if (p.startsWith(prefix)) {
|
||||
var rel = p.substring(prefix_len)
|
||||
if (rel.length == 0) continue
|
||||
|
||||
arrfor(all, function(p) {
|
||||
if (starts_with(p, prefix)) {
|
||||
var rel = text(p, prefix_len)
|
||||
if (length(rel) == 0) return
|
||||
|
||||
if (!check_neg(rel) && check_pos(rel)) {
|
||||
results.push(rel)
|
||||
push(results, rel)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return results
|
||||
|
||||
226
clean.ce
226
clean.ce
@@ -1,26 +1,218 @@
|
||||
// cell clean - Remove build artifacts from global shop
|
||||
// cell clean [<scope>] - Remove cached material to force refetch/rebuild
|
||||
//
|
||||
// Usage:
|
||||
// cell clean Clean build outputs for current directory package
|
||||
// cell clean . Clean build outputs for current directory package
|
||||
// cell clean <locator> Clean build outputs for specific package
|
||||
// cell clean shop Clean entire shop
|
||||
// cell clean world Clean all world packages
|
||||
//
|
||||
// Options:
|
||||
// --build Remove build outputs only (default)
|
||||
// --fetch Remove fetched sources only
|
||||
// --all Remove both build outputs and fetched sources
|
||||
// --deep Apply to full dependency closure
|
||||
// --dry-run Show what would be deleted
|
||||
|
||||
var fd = use('fd')
|
||||
var shop = use('internal/shop')
|
||||
var pkg = use('package')
|
||||
var fd = use('fd')
|
||||
|
||||
var build_dir = shop.get_shop_path() + '/build'
|
||||
var scope = null
|
||||
var clean_build = false
|
||||
var clean_fetch = false
|
||||
var deep = false
|
||||
var dry_run = false
|
||||
|
||||
if (!fd.is_dir(build_dir)) {
|
||||
log.console("No build directory found at " + build_dir)
|
||||
$stop()
|
||||
return
|
||||
for (var i = 0; i < length(args); i++) {
|
||||
if (args[i] == '--build') {
|
||||
clean_build = true
|
||||
} else if (args[i] == '--fetch') {
|
||||
clean_fetch = true
|
||||
} else if (args[i] == '--all') {
|
||||
clean_build = true
|
||||
clean_fetch = true
|
||||
} else if (args[i] == '--deep') {
|
||||
deep = true
|
||||
} else if (args[i] == '--dry-run') {
|
||||
dry_run = true
|
||||
} else if (args[i] == '--help' || args[i] == '-h') {
|
||||
log.console("Usage: cell clean [<scope>] [options]")
|
||||
log.console("")
|
||||
log.console("Remove cached material to force refetch/rebuild.")
|
||||
log.console("")
|
||||
log.console("Scopes:")
|
||||
log.console(" <locator> Clean specific package")
|
||||
log.console(" shop Clean entire shop")
|
||||
log.console(" world Clean all world packages")
|
||||
log.console("")
|
||||
log.console("Options:")
|
||||
log.console(" --build Remove build outputs only (default)")
|
||||
log.console(" --fetch Remove fetched sources only")
|
||||
log.console(" --all Remove both build outputs and fetched sources")
|
||||
log.console(" --deep Apply to full dependency closure")
|
||||
log.console(" --dry-run Show what would be deleted")
|
||||
$stop()
|
||||
} else if (!starts_with(args[i], '-')) {
|
||||
scope = args[i]
|
||||
}
|
||||
}
|
||||
|
||||
log.console("Cleaning build artifacts...")
|
||||
|
||||
// Remove the build directory
|
||||
try {
|
||||
fd.rm(build_dir)
|
||||
log.console("Build directory removed: " + build_dir)
|
||||
} catch (e) {
|
||||
log.error(e)
|
||||
// Default to --build if nothing specified
|
||||
if (!clean_build && !clean_fetch) {
|
||||
clean_build = true
|
||||
}
|
||||
|
||||
log.console("Clean complete!")
|
||||
// Default scope to current directory
|
||||
if (!scope) {
|
||||
scope = '.'
|
||||
}
|
||||
|
||||
$stop()
|
||||
// Resolve local paths for single package scope
|
||||
var is_shop_scope = (scope == 'shop')
|
||||
var is_world_scope = (scope == 'world')
|
||||
|
||||
if (!is_shop_scope && !is_world_scope) {
|
||||
if (scope == '.' || starts_with(scope, './') || starts_with(scope, '../') || fd.is_dir(scope)) {
|
||||
var resolved = fd.realpath(scope)
|
||||
if (resolved) {
|
||||
scope = resolved
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var files_to_delete = []
|
||||
var dirs_to_delete = []
|
||||
|
||||
// Gather packages to clean
|
||||
var packages_to_clean = []
|
||||
|
||||
if (is_shop_scope) {
|
||||
packages_to_clean = shop.list_packages()
|
||||
} else if (is_world_scope) {
|
||||
// For now, world is the same as shop
|
||||
packages_to_clean = shop.list_packages()
|
||||
} else {
|
||||
// Single package
|
||||
push(packages_to_clean, scope)
|
||||
|
||||
if (deep) {
|
||||
try {
|
||||
var deps = pkg.gather_dependencies(scope)
|
||||
arrfor(deps, function(dep) {
|
||||
push(packages_to_clean, dep)
|
||||
})
|
||||
} catch (e) {
|
||||
// Skip if can't read dependencies
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Gather files to clean
|
||||
var lib_dir = shop.get_lib_dir()
|
||||
var build_dir = shop.get_build_dir()
|
||||
var packages_dir = replace(shop.get_package_dir(''), /\/$/, '') // Get base packages dir
|
||||
|
||||
if (clean_build) {
|
||||
if (is_shop_scope) {
|
||||
// Clean entire build and lib directories
|
||||
if (fd.is_dir(build_dir)) {
|
||||
push(dirs_to_delete, build_dir)
|
||||
}
|
||||
if (fd.is_dir(lib_dir)) {
|
||||
push(dirs_to_delete, lib_dir)
|
||||
}
|
||||
} else {
|
||||
// Clean specific package libraries
|
||||
arrfor(packages_to_clean, function(p) {
|
||||
if (p == 'core') return
|
||||
|
||||
var lib_name = shop.lib_name_for_package(p)
|
||||
var dylib_ext = '.dylib'
|
||||
var lib_path = lib_dir + '/' + lib_name + dylib_ext
|
||||
|
||||
if (fd.is_file(lib_path)) {
|
||||
push(files_to_delete, lib_path)
|
||||
}
|
||||
|
||||
// Also check for .so and .dll
|
||||
var so_path = lib_dir + '/' + lib_name + '.so'
|
||||
var dll_path = lib_dir + '/' + lib_name + '.dll'
|
||||
if (fd.is_file(so_path)) {
|
||||
push(files_to_delete, so_path)
|
||||
}
|
||||
if (fd.is_file(dll_path)) {
|
||||
push(files_to_delete, dll_path)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (clean_fetch) {
|
||||
if (is_shop_scope) {
|
||||
// Clean entire packages directory (dangerous!)
|
||||
if (fd.is_dir(packages_dir)) {
|
||||
push(dirs_to_delete, packages_dir)
|
||||
}
|
||||
} else {
|
||||
// Clean specific package directories
|
||||
arrfor(packages_to_clean, function(p) {
|
||||
if (p == 'core') return
|
||||
|
||||
var pkg_dir = shop.get_package_dir(p)
|
||||
if (fd.is_dir(pkg_dir) || fd.is_link(pkg_dir)) {
|
||||
push(dirs_to_delete, pkg_dir)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Execute or report
|
||||
if (dry_run) {
|
||||
log.console("Would delete:")
|
||||
if (length(files_to_delete) == 0 && length(dirs_to_delete) == 0) {
|
||||
log.console(" (nothing to clean)")
|
||||
} else {
|
||||
arrfor(files_to_delete, function(f) {
|
||||
log.console(" [file] " + f)
|
||||
})
|
||||
arrfor(dirs_to_delete, function(d) {
|
||||
log.console(" [dir] " + d)
|
||||
})
|
||||
}
|
||||
} else {
|
||||
var deleted_count = 0
|
||||
|
||||
arrfor(files_to_delete, function(f) {
|
||||
try {
|
||||
fd.unlink(f)
|
||||
log.console("Deleted: " + f)
|
||||
deleted_count++
|
||||
} catch (e) {
|
||||
log.error("Failed to delete " + f + ": " + e)
|
||||
}
|
||||
})
|
||||
|
||||
arrfor(dirs_to_delete, function(d) {
|
||||
try {
|
||||
if (fd.is_link(d)) {
|
||||
fd.unlink(d)
|
||||
} else {
|
||||
fd.rmdir(d, 1) // recursive
|
||||
}
|
||||
log.console("Deleted: " + d)
|
||||
deleted_count++
|
||||
} catch (e) {
|
||||
log.error("Failed to delete " + d + ": " + e)
|
||||
}
|
||||
})
|
||||
|
||||
if (deleted_count == 0) {
|
||||
log.console("Nothing to clean.")
|
||||
} else {
|
||||
log.console("")
|
||||
log.console("Clean complete: " + text(deleted_count) + " item(s) deleted.")
|
||||
}
|
||||
}
|
||||
|
||||
$stop()
|
||||
|
||||
27
clone.ce
27
clone.ce
@@ -7,7 +7,7 @@ var fd = use('fd')
|
||||
var http = use('http')
|
||||
var miniz = use('miniz')
|
||||
|
||||
if (args.length < 2) {
|
||||
if (length(args) < 2) {
|
||||
log.console("Usage: cell clone <origin> <path>")
|
||||
log.console("Clones a cell package to a local path and links it.")
|
||||
$stop()
|
||||
@@ -18,7 +18,7 @@ var origin = args[0]
|
||||
var target_path = args[1]
|
||||
|
||||
// Resolve target path to absolute
|
||||
if (target_path == '.' || target_path.startsWith('./') || target_path.startsWith('../')) {
|
||||
if (target_path == '.' || starts_with(target_path, './') || starts_with(target_path, '../')) {
|
||||
var resolved = fd.realpath(target_path)
|
||||
if (resolved) {
|
||||
target_path = resolved
|
||||
@@ -27,12 +27,12 @@ if (target_path == '.' || target_path.startsWith('./') || target_path.startsWith
|
||||
var cwd = fd.realpath('.')
|
||||
if (target_path == '.') {
|
||||
target_path = cwd
|
||||
} else if (target_path.startsWith('./')) {
|
||||
target_path = cwd + target_path.substring(1)
|
||||
} else if (target_path.startsWith('../')) {
|
||||
} else if (starts_with(target_path, './')) {
|
||||
target_path = cwd + text(target_path, 1)
|
||||
} else if (starts_with(target_path, '../')) {
|
||||
// Go up one directory from cwd
|
||||
var parent = cwd.substring(0, cwd.lastIndexOf('/'))
|
||||
target_path = parent + target_path.substring(2)
|
||||
var parent = fd.dirname(cwd)
|
||||
target_path = parent + text(target_path, 2)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -92,14 +92,13 @@ try {
|
||||
for (var i = 0; i < count; i++) {
|
||||
if (zip.is_directory(i)) continue
|
||||
var filename = zip.get_filename(i)
|
||||
var parts = filename.split('/')
|
||||
if (parts.length <= 1) continue
|
||||
|
||||
// Skip the first directory (repo-commit prefix)
|
||||
parts.shift()
|
||||
var rel_path = parts.join('/')
|
||||
var first_slash = search(filename, '/')
|
||||
if (first_slash == null) continue
|
||||
if (first_slash + 1 >= length(filename)) continue
|
||||
|
||||
var rel_path = text(filename, first_slash + 1)
|
||||
var full_path = target_path + '/' + rel_path
|
||||
var dir_path = full_path.substring(0, full_path.lastIndexOf('/'))
|
||||
var dir_path = fd.dirname(full_path)
|
||||
|
||||
// Ensure directory exists
|
||||
if (!fd.is_dir(dir_path)) {
|
||||
|
||||
73
config.ce
73
config.ce
@@ -31,30 +31,30 @@ function print_help() {
|
||||
|
||||
// Parse a dot-notation key into path segments
|
||||
function parse_key(key) {
|
||||
return key.split('.')
|
||||
return array(key, '.')
|
||||
}
|
||||
|
||||
// Get a value from nested object using path
|
||||
function get_nested(obj, path) {
|
||||
var current = obj
|
||||
for (var segment of path) {
|
||||
if (!current || typeof current != 'object') return null
|
||||
arrfor(path, function(segment) {
|
||||
if (is_null(current) || !is_object(current)) return null
|
||||
current = current[segment]
|
||||
}
|
||||
})
|
||||
return current
|
||||
}
|
||||
|
||||
// Set a value in nested object using path
|
||||
function set_nested(obj, path, value) {
|
||||
var current = obj
|
||||
for (var i = 0; i < path.length - 1; i++) {
|
||||
for (var i = 0; i < length(path) - 1; i++) {
|
||||
var segment = path[i]
|
||||
if (!current[segment] || typeof current[segment] != 'object') {
|
||||
if (is_null(current[segment]) || !is_object(current[segment])) {
|
||||
current[segment] = {}
|
||||
}
|
||||
current = current[segment]
|
||||
}
|
||||
current[path[path.length - 1]] = value
|
||||
current[path[length(path) - 1]] = value
|
||||
}
|
||||
|
||||
// Parse value string into appropriate type
|
||||
@@ -64,7 +64,7 @@ function parse_value(str) {
|
||||
if (str == 'false') return false
|
||||
|
||||
// Number (including underscores)
|
||||
var num_str = str.replace(/_/g, '')
|
||||
var num_str = replace(str, /_/g, '')
|
||||
if (/^-?\d+$/.test(num_str)) return parseInt(num_str)
|
||||
if (/^-?\d*\.\d+$/.test(num_str)) return parseFloat(num_str)
|
||||
|
||||
@@ -74,29 +74,29 @@ function parse_value(str) {
|
||||
|
||||
// Format value for display
|
||||
function format_value(val) {
|
||||
if (typeof val == 'string') return '"' + val + '"'
|
||||
if (typeof val == 'number' && val >= 1000) {
|
||||
if (is_text(val)) return '"' + val + '"'
|
||||
if (is_number(val) && val >= 1000) {
|
||||
// Add underscores to large numbers
|
||||
return val.toString().replace(/\B(?=(\d{3})+(?!\d))/g, '_')
|
||||
return replace(val.toString(), /\B(?=(\d{3})+(?!\d))/g, '_')
|
||||
}
|
||||
return text(val)
|
||||
}
|
||||
|
||||
// Print configuration tree recursively
|
||||
function print_config(obj, prefix = '') {
|
||||
for (var key in obj) {
|
||||
arrfor(array(obj), function(key) {
|
||||
var val = obj[key]
|
||||
var full_key = prefix ? prefix + '.' + key : key
|
||||
|
||||
if (isa(val, object))
|
||||
if (is_object(val))
|
||||
print_config(val, full_key)
|
||||
else
|
||||
log.console(full_key + ' = ' + format_value(val))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Main command handling
|
||||
if (args.length == 0) {
|
||||
if (length(args) == 0) {
|
||||
print_help()
|
||||
$stop()
|
||||
return
|
||||
@@ -110,6 +110,9 @@ if (!config) {
|
||||
}
|
||||
|
||||
var command = args[0]
|
||||
var key
|
||||
var path
|
||||
var value
|
||||
|
||||
switch (command) {
|
||||
case 'help':
|
||||
@@ -125,14 +128,14 @@ switch (command) {
|
||||
break
|
||||
|
||||
case 'get':
|
||||
if (args.length < 2) {
|
||||
if (length(args) < 2) {
|
||||
log.error("Usage: cell config get <key>")
|
||||
$stop()
|
||||
return
|
||||
}
|
||||
var key = args[1]
|
||||
var path = parse_key(key)
|
||||
var value = get_nested(config, path)
|
||||
key = args[1]
|
||||
path = parse_key(key)
|
||||
value = get_nested(config, path)
|
||||
|
||||
if (value == null) {
|
||||
log.error("Key not found: " + key)
|
||||
@@ -143,9 +146,9 @@ switch (command) {
|
||||
log.console(key + ' = ' + format_value(value))
|
||||
}
|
||||
break
|
||||
|
||||
|
||||
case 'set':
|
||||
if (args.length < 3) {
|
||||
if (length(args) < 3) {
|
||||
log.error("Usage: cell config set <key> <value>")
|
||||
$stop()
|
||||
return
|
||||
@@ -161,8 +164,8 @@ switch (command) {
|
||||
'ar_timer', 'actor_memory', 'net_service',
|
||||
'reply_timeout', 'actor_max', 'stack_max'
|
||||
]
|
||||
if (!valid_system_keys.includes(path[1])) {
|
||||
log.error("Invalid system key. Valid keys: " + valid_system_keys.join(', '))
|
||||
if (find(valid_system_keys, path[1]) == null) {
|
||||
log.error("Invalid system key. Valid keys: " + text(valid_system_keys, ', '))
|
||||
$stop()
|
||||
return
|
||||
}
|
||||
@@ -172,10 +175,10 @@ switch (command) {
|
||||
pkg.save_config(config)
|
||||
log.console("Set " + key + " = " + format_value(value))
|
||||
break
|
||||
|
||||
|
||||
case 'actor':
|
||||
// Handle actor-specific configuration
|
||||
if (args.length < 3) {
|
||||
if (length(args) < 3) {
|
||||
log.error("Usage: cell config actor <name> <command> [options]")
|
||||
$stop()
|
||||
return
|
||||
@@ -190,7 +193,7 @@ switch (command) {
|
||||
|
||||
switch (actor_cmd) {
|
||||
case 'list':
|
||||
if (array(config.actors[actor_name]).length == 0) {
|
||||
if (length(array(config.actors[actor_name])) == 0) {
|
||||
log.console("No configuration for actor: " + actor_name)
|
||||
} else {
|
||||
log.console("# Configuration for actor: " + actor_name)
|
||||
@@ -200,14 +203,14 @@ switch (command) {
|
||||
break
|
||||
|
||||
case 'get':
|
||||
if (args.length < 4) {
|
||||
if (length(args) < 4) {
|
||||
log.error("Usage: cell config actor <name> get <key>")
|
||||
$stop()
|
||||
return
|
||||
}
|
||||
var key = args[3]
|
||||
var path = parse_key(key)
|
||||
var value = get_nested(config.actors[actor_name], path)
|
||||
key = args[3]
|
||||
path = parse_key(key)
|
||||
value = get_nested(config.actors[actor_name], path)
|
||||
|
||||
if (value == null) {
|
||||
log.error("Key not found for actor " + actor_name + ": " + key)
|
||||
@@ -217,21 +220,21 @@ switch (command) {
|
||||
break
|
||||
|
||||
case 'set':
|
||||
if (args.length < 5) {
|
||||
if (length(args) < 5) {
|
||||
log.error("Usage: cell config actor <name> set <key> <value>")
|
||||
$stop()
|
||||
return
|
||||
}
|
||||
var key = args[3]
|
||||
key = args[3]
|
||||
var value_str = args[4]
|
||||
var path = parse_key(key)
|
||||
var value = parse_value(value_str)
|
||||
path = parse_key(key)
|
||||
value = parse_value(value_str)
|
||||
|
||||
set_nested(config.actors[actor_name], path, value)
|
||||
pkg.save_config(config)
|
||||
log.console("Set actors." + actor_name + "." + key + " = " + format_value(value))
|
||||
break
|
||||
|
||||
|
||||
default:
|
||||
log.error("Unknown actor command: " + actor_cmd)
|
||||
log.console("Valid commands: list, get, set")
|
||||
|
||||
2
crypto.c
2
crypto.c
@@ -231,7 +231,7 @@ JSValue js_crypto_unlock(JSContext *js, JSValue self, int argc, JSValue *argv) {
|
||||
|
||||
static const JSCFunctionListEntry js_crypto_funcs[] = {
|
||||
JS_CFUNC_DEF("shared", 2, js_crypto_shared),
|
||||
JS_CFUNC_DEF("blake2", 1, js_crypto_blake2),
|
||||
JS_CFUNC_DEF("blake2", 2, js_crypto_blake2),
|
||||
JS_CFUNC_DEF("sign", 2, js_crypto_sign),
|
||||
JS_CFUNC_DEF("verify", 3, js_crypto_verify),
|
||||
JS_CFUNC_DEF("lock", 3, js_crypto_lock),
|
||||
|
||||
54
debug/js.c
54
debug/js.c
@@ -1,8 +1,6 @@
|
||||
#include "cell.h"
|
||||
|
||||
JSC_CCALL(os_gc, JS_RunGC(JS_GetRuntime(js)) )
|
||||
JSC_CCALL(os_mem_limit, JS_SetMemoryLimit(JS_GetRuntime(js), js2number(js,argv[0])))
|
||||
JSC_CCALL(os_gc_threshold, JS_SetGCThreshold(JS_GetRuntime(js), js2number(js,argv[0])))
|
||||
JSC_CCALL(os_max_stacksize, JS_SetMaxStackSize(JS_GetRuntime(js), js2number(js,argv[0])))
|
||||
|
||||
// Compute the approximate size of a single JS value in memory.
|
||||
@@ -15,8 +13,6 @@ JSC_CCALL(os_calc_mem,
|
||||
JS_SetPropertyStr(js,ret,"memory_used_size",number2js(js,mu.memory_used_size));
|
||||
JS_SetPropertyStr(js,ret,"malloc_count",number2js(js,mu.malloc_count));
|
||||
JS_SetPropertyStr(js,ret,"memory_used_count",number2js(js,mu.memory_used_count));
|
||||
JS_SetPropertyStr(js,ret,"atom_count",number2js(js,mu.atom_count));
|
||||
JS_SetPropertyStr(js,ret,"atom_size",number2js(js,mu.atom_size));
|
||||
JS_SetPropertyStr(js,ret,"str_count",number2js(js,mu.str_count));
|
||||
JS_SetPropertyStr(js,ret,"str_size",number2js(js,mu.str_size));
|
||||
JS_SetPropertyStr(js,ret,"obj_count",number2js(js,mu.obj_count));
|
||||
@@ -38,47 +34,6 @@ JSC_CCALL(os_calc_mem,
|
||||
JS_SetPropertyStr(js,ret,"binary_object_size",number2js(js,mu.binary_object_size));
|
||||
)
|
||||
|
||||
// Evaluate a string of JavaScript code in the current QuickJS context.
|
||||
JSC_SSCALL(os_eval,
|
||||
if (!str2) return JS_ThrowReferenceError(js, "Second argument should be the script.");
|
||||
if (!str) return JS_ThrowReferenceError(js, "First argument should be the name of the script.");
|
||||
ret = JS_Eval(js,str2,strlen(str2),str, 0);
|
||||
)
|
||||
|
||||
// Compile a string of JavaScript code into a function object.
|
||||
JSC_SSCALL(js_compile,
|
||||
if (!str2) return JS_ThrowReferenceError(js, "Second argument should be the script.");
|
||||
if (!str) return JS_ThrowReferenceError(js, "First argument should be the name of the script.");
|
||||
ret = JS_Eval(js, str2, strlen(str2), str, JS_EVAL_FLAG_COMPILE_ONLY | JS_EVAL_FLAG_BACKTRACE_BARRIER);
|
||||
)
|
||||
|
||||
// Evaluate a function object in the current QuickJS context.
|
||||
JSC_CCALL(js_eval_compile,
|
||||
JS_DupValue(js,argv[0]);
|
||||
ret = JS_EvalFunction(js, argv[0]);
|
||||
)
|
||||
|
||||
// Compile a function object into a bytecode blob.
|
||||
JSC_CCALL(js_compile_blob,
|
||||
size_t size;
|
||||
uint8_t *data = JS_WriteObject(js, &size, argv[0], JS_WRITE_OBJ_BYTECODE);
|
||||
if (!data) {
|
||||
return JS_ThrowInternalError(js, "Failed to serialize bytecode");
|
||||
}
|
||||
ret = js_new_blob_stoned_copy(js, data, size);
|
||||
js_free(js, data);
|
||||
)
|
||||
|
||||
// Compile a bytecode blob into a function object.
|
||||
JSC_CCALL(js_compile_unblob,
|
||||
size_t size;
|
||||
void *data = js_get_blob_data(js, &size, argv[0]);
|
||||
if (data == -1) return JS_EXCEPTION;
|
||||
if (!data) return JS_ThrowReferenceError(js, "No data present in blob.");
|
||||
|
||||
return JS_ReadObject(js, data, size, JS_READ_OBJ_BYTECODE);
|
||||
)
|
||||
|
||||
// Disassemble a function object into a string.
|
||||
JSC_CCALL(js_disassemble,
|
||||
return js_debugger_fn_bytecode(js, argv[0]);
|
||||
@@ -92,14 +47,7 @@ JSC_CCALL(js_fn_info,
|
||||
static const JSCFunctionListEntry js_js_funcs[] = {
|
||||
MIST_FUNC_DEF(os, calc_mem, 0),
|
||||
MIST_FUNC_DEF(os, mem_limit, 1),
|
||||
MIST_FUNC_DEF(os, gc_threshold, 1),
|
||||
MIST_FUNC_DEF(os, max_stacksize, 1),
|
||||
MIST_FUNC_DEF(os, gc, 0),
|
||||
MIST_FUNC_DEF(os, eval, 2),
|
||||
MIST_FUNC_DEF(js, compile, 2),
|
||||
MIST_FUNC_DEF(js, eval_compile, 1),
|
||||
MIST_FUNC_DEF(js, compile_blob, 1),
|
||||
MIST_FUNC_DEF(js, compile_unblob, 1),
|
||||
MIST_FUNC_DEF(js, disassemble, 1),
|
||||
MIST_FUNC_DEF(js, fn_info, 1),
|
||||
};
|
||||
@@ -108,4 +56,4 @@ JSValue js_js_use(JSContext *js) {
|
||||
JSValue mod = JS_NewObject(js);
|
||||
JS_SetPropertyFunctionList(js,mod,js_js_funcs,countof(js_js_funcs));
|
||||
return mod;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
nav:
|
||||
- index.md
|
||||
- cellscript.md
|
||||
- actors.md
|
||||
- packages.md
|
||||
- cli.md
|
||||
- c-modules.md
|
||||
- Standard Library: library
|
||||
|
||||
90
docs/_index.md
Normal file
90
docs/_index.md
Normal file
@@ -0,0 +1,90 @@
|
||||
---
|
||||
title: "Documentation"
|
||||
description: "ƿit language documentation"
|
||||
type: "docs"
|
||||
---
|
||||
|
||||

|
||||
|
||||
ƿit is an actor-based scripting language for building concurrent applications. It combines a familiar C-like syntax with the actor model of computation, optimized for low memory usage and simplicity.
|
||||
|
||||
## Key Features
|
||||
|
||||
- **Actor Model** — isolated memory, message passing, no shared state
|
||||
- **Immutability** — `stone()` makes values permanently frozen
|
||||
- **Prototype Inheritance** — objects without classes
|
||||
- **C Integration** — seamlessly extend with native code
|
||||
- **Cross-Platform** — deploy to desktop, web, and embedded
|
||||
|
||||
## Quick Start
|
||||
|
||||
```javascript
|
||||
// hello.ce - A simple actor
|
||||
print("Hello, ƿit!")
|
||||
$stop()
|
||||
```
|
||||
|
||||
```bash
|
||||
pit hello
|
||||
```
|
||||
|
||||
## Language
|
||||
|
||||
- [**ƿit Language**](/docs/language/) — syntax, types, and operators
|
||||
- [**Actors and Modules**](/docs/actors/) — the execution model
|
||||
- [**Requestors**](/docs/requestors/) — asynchronous composition
|
||||
- [**Packages**](/docs/packages/) — code organization and sharing
|
||||
|
||||
## Reference
|
||||
|
||||
- [**Built-in Functions**](/docs/functions/) — intrinsics reference
|
||||
- [text](/docs/library/text/) — text conversion and manipulation
|
||||
- [number](/docs/library/number/) — numeric conversion and operations
|
||||
- [array](/docs/library/array/) — array creation and manipulation
|
||||
- [object](/docs/library/object/) — object creation, prototypes, and serialization
|
||||
|
||||
## Standard Library
|
||||
|
||||
Modules loaded with `use()`:
|
||||
|
||||
- [blob](/docs/library/blob/) — binary data
|
||||
- [time](/docs/library/time/) — time and dates
|
||||
- [math](/docs/library/math/) — trigonometry and math
|
||||
- [json](/docs/library/json/) — JSON encoding/decoding
|
||||
- [random](/docs/library/random/) — random numbers
|
||||
|
||||
## Tools
|
||||
|
||||
- [**Command Line**](/docs/cli/) — the `pit` tool
|
||||
- [**Writing C Modules**](/docs/c-modules/) — native extensions
|
||||
|
||||
## Architecture
|
||||
|
||||
ƿit programs are organized into **packages**. Each package contains:
|
||||
|
||||
- **Modules** (`.cm`) — return a value, cached and frozen
|
||||
- **Actors** (`.ce`) — run independently, communicate via messages
|
||||
- **C files** (`.c`) — compiled to native libraries
|
||||
|
||||
Actors never share memory. They communicate by sending messages, which are automatically serialized. This makes concurrent programming safe and predictable.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
# Clone and bootstrap
|
||||
git clone https://gitea.pockle.world/john/cell
|
||||
cd cell
|
||||
make bootstrap
|
||||
```
|
||||
|
||||
The ƿit shop is stored at `~/.pit/`.
|
||||
|
||||
## Development
|
||||
|
||||
After making changes, recompile with:
|
||||
|
||||
```bash
|
||||
make
|
||||
```
|
||||
|
||||
Run `cell --help` to see all available CLI flags.
|
||||
@@ -1,10 +1,15 @@
|
||||
# Actors and Modules
|
||||
---
|
||||
title: "Actors and Modules"
|
||||
description: "The ƿit execution model"
|
||||
weight: 20
|
||||
type: "docs"
|
||||
---
|
||||
|
||||
Cell organizes code into two types of scripts: **modules** (`.cm`) and **actors** (`.ce`).
|
||||
ƿit organizes code into two types of scripts: **modules** (`.cm`) and **actors** (`.ce`).
|
||||
|
||||
## The Actor Model
|
||||
|
||||
Cell is built on the actor model of computation. Each actor:
|
||||
ƿit is built on the actor model of computation. Each actor:
|
||||
|
||||
- Has its own **isolated memory** — actors never share state
|
||||
- Runs to completion each **turn** — no preemption
|
||||
@@ -21,13 +26,13 @@ A module is a script that **returns a value**. The returned value is cached and
|
||||
// math_utils.cm
|
||||
var math = use('math/radians')
|
||||
|
||||
function distance(x1, y1, x2, y2) {
|
||||
var distance = function(x1, y1, x2, y2) {
|
||||
var dx = x2 - x1
|
||||
var dy = y2 - y1
|
||||
return math.sqrt(dx * dx + dy * dy)
|
||||
}
|
||||
|
||||
function midpoint(x1, y1, x2, y2) {
|
||||
var midpoint = function(x1, y1, x2, y2) {
|
||||
return {
|
||||
x: (x1 + x2) / 2,
|
||||
y: (y1 + y2) / 2
|
||||
@@ -60,12 +65,12 @@ An actor is a script that **does not return a value**. It runs as an independent
|
||||
|
||||
```javascript
|
||||
// worker.ce
|
||||
log.console("Worker started")
|
||||
print("Worker started")
|
||||
|
||||
$on_message = function(msg) {
|
||||
log.console("Received:", msg)
|
||||
$receiver(function(msg, reply) {
|
||||
print("Received:", msg)
|
||||
// Process message...
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
**Key properties:**
|
||||
@@ -83,7 +88,7 @@ Actors have access to special functions prefixed with `$`:
|
||||
Reference to the current actor.
|
||||
|
||||
```javascript
|
||||
log.console($me) // actor reference
|
||||
print($me) // actor reference
|
||||
```
|
||||
|
||||
### $stop()
|
||||
@@ -100,7 +105,7 @@ Send a message to another actor.
|
||||
|
||||
```javascript
|
||||
$send(other_actor, {type: "ping", data: 42}, function(reply) {
|
||||
log.console("Got reply:", reply)
|
||||
print("Got reply:", reply)
|
||||
})
|
||||
```
|
||||
|
||||
@@ -112,7 +117,7 @@ Start a new actor from a script.
|
||||
|
||||
```javascript
|
||||
$start(function(new_actor) {
|
||||
log.console("Started:", new_actor)
|
||||
print("Started:", new_actor)
|
||||
}, "worker")
|
||||
```
|
||||
|
||||
@@ -122,7 +127,7 @@ Schedule a callback after a delay.
|
||||
|
||||
```javascript
|
||||
$delay(function() {
|
||||
log.console("5 seconds later")
|
||||
print("5 seconds later")
|
||||
}, 5)
|
||||
```
|
||||
|
||||
@@ -169,19 +174,47 @@ $contact(function(connection) {
|
||||
|
||||
### $time_limit(requestor, seconds)
|
||||
|
||||
Wrap a requestor with a timeout.
|
||||
Wrap a requestor with a timeout. See [Requestors](/docs/requestors/) for details.
|
||||
|
||||
```javascript
|
||||
$time_limit(my_requestor, 10) // 10 second timeout
|
||||
```
|
||||
|
||||
### $couple(actor)
|
||||
|
||||
Couple the current actor to another actor. When the coupled actor dies, the current actor also dies. Coupling is automatic between an actor and its overling (parent).
|
||||
|
||||
```javascript
|
||||
$couple(other_actor)
|
||||
```
|
||||
|
||||
### $unneeded(callback, seconds)
|
||||
|
||||
Schedule the actor for removal after a specified time.
|
||||
|
||||
```javascript
|
||||
$unneeded(function() {
|
||||
// cleanup before removal
|
||||
}, 30)
|
||||
```
|
||||
|
||||
### $connection(callback, actor, config)
|
||||
|
||||
Get information about the connection to another actor, such as latency, bandwidth, and activity.
|
||||
|
||||
```javascript
|
||||
$connection(function(info) {
|
||||
print(info.latency)
|
||||
}, other_actor, {})
|
||||
```
|
||||
|
||||
## Module Resolution
|
||||
|
||||
When you call `use('name')`, Cell searches:
|
||||
When you call `use('name')`, ƿit searches:
|
||||
|
||||
1. **Current package** — files relative to package root
|
||||
2. **Dependencies** — packages declared in `cell.toml`
|
||||
3. **Core** — built-in Cell modules
|
||||
2. **Dependencies** — packages declared in `pit.toml`
|
||||
3. **Core** — built-in ƿit modules
|
||||
|
||||
```javascript
|
||||
// From within package 'myapp':
|
||||
@@ -199,14 +232,14 @@ Files starting with underscore (`_helper.cm`) are private to the package.
|
||||
// main.ce - Entry point
|
||||
var config = use('config')
|
||||
|
||||
log.console("Starting application...")
|
||||
print("Starting application...")
|
||||
|
||||
$start(function(worker) {
|
||||
$send(worker, {task: "process", data: [1, 2, 3]})
|
||||
}, "worker")
|
||||
|
||||
$delay(function() {
|
||||
log.console("Shutting down")
|
||||
print("Shutting down")
|
||||
$stop()
|
||||
}, 10)
|
||||
```
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
# Writing C Modules
|
||||
---
|
||||
title: "Writing C Modules"
|
||||
description: "Extending ƿit with native code"
|
||||
weight: 50
|
||||
type: "docs"
|
||||
---
|
||||
|
||||
Cell makes it easy to extend functionality with C code. C files in a package are compiled into a dynamic library and can be imported like any other module.
|
||||
ƿit makes it easy to extend functionality with C code. C files in a package are compiled into a dynamic library and can be imported like any other module.
|
||||
|
||||
## Basic Structure
|
||||
|
||||
@@ -45,12 +50,12 @@ Where:
|
||||
- `<filename>` is the C file name without extension
|
||||
|
||||
Examples:
|
||||
- `mypackage/math.c` → `js_mypackage_math_use`
|
||||
- `gitea.pockle.world/john/lib/render.c` → `js_gitea_pockle_world_john_lib_render_use`
|
||||
- `mypackage/math.c` -> `js_mypackage_math_use`
|
||||
- `gitea.pockle.world/john/lib/render.c` -> `js_gitea_pockle_world_john_lib_render_use`
|
||||
|
||||
## Required Headers
|
||||
|
||||
Include `cell.h` for all Cell integration:
|
||||
Include `cell.h` for all ƿit integration:
|
||||
|
||||
```c
|
||||
#include "cell.h"
|
||||
@@ -63,7 +68,7 @@ This provides:
|
||||
|
||||
## Conversion Functions
|
||||
|
||||
### JavaScript ↔ C
|
||||
### JavaScript <-> C
|
||||
|
||||
```c
|
||||
// Numbers
|
||||
@@ -201,7 +206,7 @@ static const JSCFunctionListEntry js_funcs[] = {
|
||||
CELL_USE_FUNCS(js_funcs)
|
||||
```
|
||||
|
||||
Usage in Cell:
|
||||
Usage in ƿit:
|
||||
|
||||
```javascript
|
||||
var vector = use('vector')
|
||||
@@ -211,7 +216,7 @@ var n = vector.normalize(3, 4) // {x: 0.6, y: 0.8}
|
||||
var d = vector.dot(1, 0, 0, 1) // 0
|
||||
```
|
||||
|
||||
## Combining C and Cell
|
||||
## Combining C and ƿit
|
||||
|
||||
A common pattern is to have a C file provide low-level functions and a `.cm` file provide a higher-level API:
|
||||
|
||||
@@ -224,7 +229,7 @@ A common pattern is to have a C file provide low-level functions and a `.cm` fil
|
||||
// vector.cm
|
||||
var native = this // C module passed as 'this'
|
||||
|
||||
function Vector(x, y) {
|
||||
var Vector = function(x, y) {
|
||||
return {x: x, y: y}
|
||||
}
|
||||
|
||||
@@ -244,11 +249,11 @@ return Vector
|
||||
C files are automatically compiled when you run:
|
||||
|
||||
```bash
|
||||
cell build
|
||||
cell update
|
||||
pit build
|
||||
pit update
|
||||
```
|
||||
|
||||
The resulting dynamic library is placed in `~/.cell/lib/`.
|
||||
The resulting dynamic library is placed in `~/.pit/lib/`.
|
||||
|
||||
## Platform-Specific Code
|
||||
|
||||
@@ -260,7 +265,7 @@ audio_playdate.c # Playdate
|
||||
audio_emscripten.c # Web/Emscripten
|
||||
```
|
||||
|
||||
Cell selects the appropriate file based on the target platform.
|
||||
ƿit selects the appropriate file based on the target platform.
|
||||
|
||||
## Static Declarations
|
||||
|
||||
|
||||
@@ -1,288 +0,0 @@
|
||||
# Cell Language
|
||||
|
||||
Cell is a scripting language for actor-based programming. It combines a familiar syntax with a prototype-based object system and strict immutability semantics.
|
||||
|
||||
## Basics
|
||||
|
||||
### Variables and Constants
|
||||
|
||||
```javascript
|
||||
var x = 10 // mutable variable (block-scoped like let)
|
||||
def PI = 3.14159 // constant (cannot be reassigned)
|
||||
```
|
||||
|
||||
### Data Types
|
||||
|
||||
Cell has six fundamental types:
|
||||
|
||||
- **number** — DEC64 decimal floating point (no rounding errors)
|
||||
- **text** — Unicode strings
|
||||
- **logical** — `true` or `false`
|
||||
- **null** — the absence of a value (no `undefined`)
|
||||
- **array** — ordered, numerically-indexed sequences
|
||||
- **object** — key-value records with prototype inheritance
|
||||
- **blob** — binary data (bits, not bytes)
|
||||
- **function** — first-class callable values
|
||||
|
||||
### Literals
|
||||
|
||||
```javascript
|
||||
// Numbers
|
||||
42
|
||||
3.14
|
||||
1_000_000 // underscores for readability
|
||||
|
||||
// Text
|
||||
"hello"
|
||||
'world'
|
||||
`template ${x}` // string interpolation
|
||||
|
||||
// Logical
|
||||
true
|
||||
false
|
||||
|
||||
// Null
|
||||
null
|
||||
|
||||
// Arrays
|
||||
[1, 2, 3]
|
||||
["a", "b", "c"]
|
||||
|
||||
// Objects
|
||||
{name: "cell", version: 1}
|
||||
{x: 10, y: 20}
|
||||
```
|
||||
|
||||
### Operators
|
||||
|
||||
```javascript
|
||||
// Arithmetic
|
||||
+ - * / %
|
||||
** // exponentiation
|
||||
|
||||
// Comparison (always strict)
|
||||
== // equals (like === in JS)
|
||||
!= // not equals (like !== in JS)
|
||||
< > <= >=
|
||||
|
||||
// Logical
|
||||
&& || !
|
||||
|
||||
// Assignment
|
||||
= += -= *= /=
|
||||
```
|
||||
|
||||
### Control Flow
|
||||
|
||||
```javascript
|
||||
// Conditionals
|
||||
if (x > 0) {
|
||||
log.console("positive")
|
||||
} else if (x < 0) {
|
||||
log.console("negative")
|
||||
} else {
|
||||
log.console("zero")
|
||||
}
|
||||
|
||||
// Ternary
|
||||
var sign = x > 0 ? 1 : -1
|
||||
|
||||
// Loops
|
||||
for (var i = 0; i < 10; i++) {
|
||||
log.console(i)
|
||||
}
|
||||
|
||||
for (var item of items) {
|
||||
log.console(item)
|
||||
}
|
||||
|
||||
for (var key in obj) {
|
||||
log.console(key, obj[key])
|
||||
}
|
||||
|
||||
while (condition) {
|
||||
// body
|
||||
}
|
||||
|
||||
// Control
|
||||
break
|
||||
continue
|
||||
return value
|
||||
throw "error message"
|
||||
```
|
||||
|
||||
### Functions
|
||||
|
||||
```javascript
|
||||
// Named function
|
||||
function add(a, b) {
|
||||
return a + b
|
||||
}
|
||||
|
||||
// Anonymous function
|
||||
var multiply = function(a, b) {
|
||||
return a * b
|
||||
}
|
||||
|
||||
// Arrow function
|
||||
var square = x => x * x
|
||||
var sum = (a, b) => a + b
|
||||
|
||||
// Rest parameters
|
||||
function log_all(...args) {
|
||||
for (var arg of args) log.console(arg)
|
||||
}
|
||||
|
||||
// Default parameters
|
||||
function greet(name, greeting = "Hello") {
|
||||
return `${greeting}, ${name}!`
|
||||
}
|
||||
```
|
||||
|
||||
All closures capture `this` (like arrow functions in JavaScript).
|
||||
|
||||
## Arrays
|
||||
|
||||
Arrays are **distinct from objects**. They are ordered, numerically-indexed sequences. You cannot add arbitrary string keys to an array.
|
||||
|
||||
```javascript
|
||||
var arr = [1, 2, 3]
|
||||
arr[0] // 1
|
||||
arr[2] = 10 // [1, 2, 10]
|
||||
length(arr) // 3
|
||||
|
||||
// Array spread
|
||||
var more = [...arr, 4, 5] // [1, 2, 10, 4, 5]
|
||||
```
|
||||
|
||||
## Objects
|
||||
|
||||
Objects are key-value records with prototype-based inheritance.
|
||||
|
||||
```javascript
|
||||
var point = {x: 10, y: 20}
|
||||
point.x // 10
|
||||
point["y"] // 20
|
||||
|
||||
// Object spread
|
||||
var point3d = {...point, z: 30}
|
||||
|
||||
// Prototype inheritance
|
||||
var colored_point = {__proto__: point, color: "red"}
|
||||
colored_point.x // 10 (inherited)
|
||||
```
|
||||
|
||||
### Prototypes
|
||||
|
||||
```javascript
|
||||
// Create object with prototype
|
||||
var child = meme(parent)
|
||||
|
||||
// Get prototype
|
||||
var p = proto(child)
|
||||
|
||||
// Check prototype chain
|
||||
isa(child, parent) // true
|
||||
```
|
||||
|
||||
## Immutability with Stone
|
||||
|
||||
The `stone()` function makes values permanently immutable.
|
||||
|
||||
```javascript
|
||||
var config = stone({
|
||||
debug: true,
|
||||
maxRetries: 3
|
||||
})
|
||||
|
||||
config.debug = false // Error! Stone objects cannot be modified
|
||||
```
|
||||
|
||||
Stone is **deep** — all nested objects and arrays are also frozen. This cannot be reversed.
|
||||
|
||||
```javascript
|
||||
stone.p(value) // returns true if value is stone
|
||||
```
|
||||
|
||||
## Built-in Functions
|
||||
|
||||
### length(value)
|
||||
|
||||
Returns the length of arrays (elements), text (codepoints), blobs (bits), or functions (arity).
|
||||
|
||||
```javascript
|
||||
length([1, 2, 3]) // 3
|
||||
length("hello") // 5
|
||||
length(function(a,b){}) // 2
|
||||
```
|
||||
|
||||
### use(path)
|
||||
|
||||
Import a module. Returns the cached, stone value.
|
||||
|
||||
```javascript
|
||||
var math = use('math/radians')
|
||||
var json = use('json')
|
||||
```
|
||||
|
||||
### isa(value, type)
|
||||
|
||||
Check type or prototype chain.
|
||||
|
||||
```javascript
|
||||
isa(42, number) // true
|
||||
isa("hi", text) // true
|
||||
isa([1,2], array) // true
|
||||
isa({}, object) // true
|
||||
isa(child, parent) // true if parent is in prototype chain
|
||||
```
|
||||
|
||||
### reverse(array)
|
||||
|
||||
Returns a new array with elements in reverse order.
|
||||
|
||||
```javascript
|
||||
reverse([1, 2, 3]) // [3, 2, 1]
|
||||
```
|
||||
|
||||
### logical(value)
|
||||
|
||||
Convert to boolean.
|
||||
|
||||
```javascript
|
||||
logical(0) // false
|
||||
logical(1) // true
|
||||
logical("true") // true
|
||||
logical("false") // false
|
||||
logical(null) // false
|
||||
```
|
||||
|
||||
## Logging
|
||||
|
||||
```javascript
|
||||
log.console("message") // standard output
|
||||
log.error("problem") // error output
|
||||
```
|
||||
|
||||
## Pattern Matching
|
||||
|
||||
Cell supports regex patterns in string functions, but not standalone regex objects.
|
||||
|
||||
```javascript
|
||||
text.search("hello world", /world/)
|
||||
text.replace("hello", /l/g, "L")
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
```javascript
|
||||
try {
|
||||
riskyOperation()
|
||||
} catch (e) {
|
||||
log.error(e)
|
||||
}
|
||||
|
||||
throw "something went wrong"
|
||||
```
|
||||
|
||||
If an actor has an uncaught error, it crashes.
|
||||
105
docs/cli.md
105
docs/cli.md
@@ -1,138 +1,143 @@
|
||||
# Command Line Interface
|
||||
---
|
||||
title: "Command Line Interface"
|
||||
description: "The pit tool"
|
||||
weight: 40
|
||||
type: "docs"
|
||||
---
|
||||
|
||||
Cell provides a command-line interface for managing packages, running scripts, and building applications.
|
||||
ƿit provides a command-line interface for managing packages, running scripts, and building applications.
|
||||
|
||||
## Basic Usage
|
||||
|
||||
```bash
|
||||
cell <command> [arguments]
|
||||
pit <command> [arguments]
|
||||
```
|
||||
|
||||
## Commands
|
||||
|
||||
### cell version
|
||||
### pit version
|
||||
|
||||
Display the Cell version.
|
||||
Display the ƿit version.
|
||||
|
||||
```bash
|
||||
cell version
|
||||
pit version
|
||||
# 0.1.0
|
||||
```
|
||||
|
||||
### cell install
|
||||
### pit install
|
||||
|
||||
Install a package to the shop.
|
||||
|
||||
```bash
|
||||
cell install gitea.pockle.world/john/prosperon
|
||||
cell install /Users/john/local/mypackage # local path
|
||||
pit install gitea.pockle.world/john/prosperon
|
||||
pit install /Users/john/local/mypackage # local path
|
||||
```
|
||||
|
||||
### cell update
|
||||
### pit update
|
||||
|
||||
Update packages from remote sources.
|
||||
|
||||
```bash
|
||||
cell update # update all packages
|
||||
cell update <package> # update specific package
|
||||
pit update # update all packages
|
||||
pit update <package> # update specific package
|
||||
```
|
||||
|
||||
### cell remove
|
||||
### pit remove
|
||||
|
||||
Remove a package from the shop.
|
||||
|
||||
```bash
|
||||
cell remove gitea.pockle.world/john/oldpackage
|
||||
pit remove gitea.pockle.world/john/oldpackage
|
||||
```
|
||||
|
||||
### cell list
|
||||
### pit list
|
||||
|
||||
List installed packages.
|
||||
|
||||
```bash
|
||||
cell list # list all installed packages
|
||||
cell list <package> # list dependencies of a package
|
||||
pit list # list all installed packages
|
||||
pit list <package> # list dependencies of a package
|
||||
```
|
||||
|
||||
### cell ls
|
||||
### pit ls
|
||||
|
||||
List modules and actors in a package.
|
||||
|
||||
```bash
|
||||
cell ls # list files in current project
|
||||
cell ls <package> # list files in specified package
|
||||
pit ls # list files in current project
|
||||
pit ls <package> # list files in specified package
|
||||
```
|
||||
|
||||
### cell build
|
||||
### pit build
|
||||
|
||||
Build the current package.
|
||||
|
||||
```bash
|
||||
cell build
|
||||
pit build
|
||||
```
|
||||
|
||||
### cell test
|
||||
### pit test
|
||||
|
||||
Run tests.
|
||||
|
||||
```bash
|
||||
cell test # run tests in current package
|
||||
cell test all # run all tests
|
||||
cell test <package> # run tests in specific package
|
||||
pit test # run tests in current package
|
||||
pit test all # run all tests
|
||||
pit test <package> # run tests in specific package
|
||||
```
|
||||
|
||||
### cell link
|
||||
### pit link
|
||||
|
||||
Manage local package links for development.
|
||||
|
||||
```bash
|
||||
cell link add <canonical> <local_path> # link a package
|
||||
cell link list # show all links
|
||||
cell link delete <canonical> # remove a link
|
||||
cell link clear # remove all links
|
||||
pit link add <canonical> <local_path> # link a package
|
||||
pit link list # show all links
|
||||
pit link delete <canonical> # remove a link
|
||||
pit link clear # remove all links
|
||||
```
|
||||
|
||||
### cell fetch
|
||||
### pit fetch
|
||||
|
||||
Fetch package sources without extracting.
|
||||
|
||||
```bash
|
||||
cell fetch <package>
|
||||
pit fetch <package>
|
||||
```
|
||||
|
||||
### cell upgrade
|
||||
### pit upgrade
|
||||
|
||||
Upgrade the Cell installation itself.
|
||||
Upgrade the ƿit installation itself.
|
||||
|
||||
```bash
|
||||
cell upgrade
|
||||
pit upgrade
|
||||
```
|
||||
|
||||
### cell clean
|
||||
### pit clean
|
||||
|
||||
Clean build artifacts.
|
||||
|
||||
```bash
|
||||
cell clean
|
||||
pit clean
|
||||
```
|
||||
|
||||
### cell help
|
||||
### pit help
|
||||
|
||||
Display help information.
|
||||
|
||||
```bash
|
||||
cell help
|
||||
cell help <command>
|
||||
pit help
|
||||
pit help <command>
|
||||
```
|
||||
|
||||
## Running Scripts
|
||||
|
||||
Any `.ce` file in the Cell core can be run as a command:
|
||||
Any `.ce` file in the ƿit core can be run as a command:
|
||||
|
||||
```bash
|
||||
cell version # runs version.ce
|
||||
cell build # runs build.ce
|
||||
cell test # runs test.ce
|
||||
pit version # runs version.ce
|
||||
pit build # runs build.ce
|
||||
pit test # runs test.ce
|
||||
```
|
||||
|
||||
## Package Locators
|
||||
@@ -143,16 +148,16 @@ Packages are identified by locators:
|
||||
- **Local**: `/absolute/path/to/package`
|
||||
|
||||
```bash
|
||||
cell install gitea.pockle.world/john/prosperon
|
||||
cell install /Users/john/work/mylib
|
||||
pit install gitea.pockle.world/john/prosperon
|
||||
pit install /Users/john/work/mylib
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
Cell stores its data in `~/.cell/`:
|
||||
ƿit stores its data in `~/.pit/`:
|
||||
|
||||
```
|
||||
~/.cell/
|
||||
~/.pit/
|
||||
├── packages/ # installed packages
|
||||
├── lib/ # compiled dynamic libraries
|
||||
├── build/ # build cache
|
||||
@@ -163,7 +168,7 @@ Cell stores its data in `~/.cell/`:
|
||||
|
||||
## Environment
|
||||
|
||||
Cell reads the `HOME` environment variable to locate the shop directory.
|
||||
ƿit reads the `HOME` environment variable to locate the shop directory.
|
||||
|
||||
## Exit Codes
|
||||
|
||||
|
||||
513
docs/functions.md
Normal file
513
docs/functions.md
Normal file
@@ -0,0 +1,513 @@
|
||||
---
|
||||
title: "Built-in Functions"
|
||||
description: "Intrinsic constants and functions"
|
||||
weight: 60
|
||||
type: "docs"
|
||||
---
|
||||
|
||||
The intrinsics are constants and functions that are built into the language. The `use` statement is not needed to access them.
|
||||
|
||||
A programmer is not obliged to consult the list of intrinsics before naming a new variable or input. New intrinsics may be added to ƿit without breaking existing programs.
|
||||
|
||||
## Constants
|
||||
|
||||
### false
|
||||
|
||||
The value of `1 == 0`. One of the two logical values.
|
||||
|
||||
### true
|
||||
|
||||
The value of `1 == 1`. One of the two logical values.
|
||||
|
||||
### null
|
||||
|
||||
The value of `1 / 0`. The null value is an empty immutable object. All attempts to obtain a value from null by refinement will produce null.
|
||||
|
||||
Any attempt to modify null will disrupt. Any attempt to call null as a function will disrupt.
|
||||
|
||||
null is the value of missing input values, missing fields in records, and invalid numbers.
|
||||
|
||||
### pi
|
||||
|
||||
An approximation of circumference / diameter: 3.1415926535897932.
|
||||
|
||||
## Creator Functions
|
||||
|
||||
The creator functions are **polymorphic** — they examine the types of their arguments to decide what to do. The first argument's type selects the behavior. All return null if their inputs are not suitable.
|
||||
|
||||
### array
|
||||
|
||||
The `array` function creates arrays from various inputs. Its behavior depends on the type of the first argument:
|
||||
|
||||
**From a number** — create an array of that size:
|
||||
|
||||
`array(number)` — All elements are initialized to null.
|
||||
|
||||
`array(number, initial_value)` — All elements are initialized to initial_value. If initial_value is a function, it is called for each element. If the function has arity >= 1, it is passed the element number.
|
||||
|
||||
```javascript
|
||||
array(3) // [null, null, null]
|
||||
array(3, 0) // [0, 0, 0]
|
||||
array(5, i => i * 2) // [0, 2, 4, 6, 8]
|
||||
```
|
||||
|
||||
**From an array** — copy, map, concat, or slice:
|
||||
|
||||
`array(array)` — Copy. Make a mutable copy of the array.
|
||||
|
||||
`array(array, function, reverse, exit)` — Map. Call the function with each element, collecting return values in a new array. The function is passed each element and its element number.
|
||||
|
||||
```javascript
|
||||
array([1, 2, 3], x => x * 10) // [10, 20, 30]
|
||||
```
|
||||
|
||||
If reverse is true, starts with the last element and works backwards.
|
||||
|
||||
If exit is not null, when the function returns the exit value, array returns early. The exit value is not stored into the new array.
|
||||
|
||||
`array(array, another_array)` — Concat. Produce a new array concatenating both.
|
||||
|
||||
```javascript
|
||||
array([1, 2], [3, 4]) // [1, 2, 3, 4]
|
||||
```
|
||||
|
||||
`array(array, from, to)` — Slice. Make a mutable copy of part of an array. If from is negative, add length(array). If to is negative, add length(array).
|
||||
|
||||
```javascript
|
||||
array([1, 2, 3, 4, 5], 1, 4) // [2, 3, 4]
|
||||
array([1, 2, 3], -2) // [2, 3]
|
||||
```
|
||||
|
||||
**From a record** — get keys:
|
||||
|
||||
`array(record)` — Keys. Make an array containing all text keys in the record.
|
||||
|
||||
```javascript
|
||||
array({a: 1, b: 2}) // ["a", "b"]
|
||||
```
|
||||
|
||||
**From text** — split into characters or substrings:
|
||||
|
||||
`array(text)` — Split text into an array of individual characters (grapheme clusters). This is the standard way to iterate over characters.
|
||||
|
||||
```javascript
|
||||
array("hello") // ["h", "e", "l", "l", "o"]
|
||||
array("ƿit") // ["ƿ", "i", "t"]
|
||||
```
|
||||
|
||||
`array(text, separator)` — Split text by a separator string into an array of subtexts.
|
||||
|
||||
```javascript
|
||||
array("a,b,c", ",") // ["a", "b", "c"]
|
||||
```
|
||||
|
||||
`array(text, length)` — Dice text into an array of subtexts of a given length.
|
||||
|
||||
### logical
|
||||
|
||||
```javascript
|
||||
logical(0) // false
|
||||
logical(false) // false
|
||||
logical("false") // false
|
||||
logical(null) // false
|
||||
logical(1) // true
|
||||
logical(true) // true
|
||||
logical("true") // true
|
||||
```
|
||||
|
||||
All other values return null.
|
||||
|
||||
### number
|
||||
|
||||
The `number` function converts values to numbers. Its behavior depends on the type of the first argument:
|
||||
|
||||
`number(logical)` — Returns 1 or 0.
|
||||
|
||||
`number(number)` — Returns the number.
|
||||
|
||||
`number(text, radix)` — Convert text to number. Radix is 2 thru 37 (default: 10).
|
||||
|
||||
`number(text, format)` — Parse formatted numbers:
|
||||
|
||||
| Format | Radix | Separator | Decimal point |
|
||||
|--------|-------|-----------|---------------|
|
||||
| `""` | 10 | | .period |
|
||||
| `"n"` | 10 | | .period |
|
||||
| `"u"` | 10 | _underbar | .period |
|
||||
| `"d"` | 10 | ,comma | .period |
|
||||
| `"s"` | 10 | space | .period |
|
||||
| `"v"` | 10 | .period | ,comma |
|
||||
| `"l"` | 10 | locale | locale |
|
||||
| `"b"` | 2 | | |
|
||||
| `"o"` | 8 | | |
|
||||
| `"h"` | 16 | | |
|
||||
| `"j"` | auto | | |
|
||||
|
||||
```javascript
|
||||
number("123,456,789.10", "d") // 123456789.1
|
||||
number("123.456.789,10", "v") // 123456789.1
|
||||
number("666", "o") // 438
|
||||
number("666", "h") // 1638
|
||||
```
|
||||
|
||||
### text
|
||||
|
||||
The `text` function converts values to text. Its behavior depends on the type of the first argument:
|
||||
|
||||
**From an array** — join elements into text:
|
||||
|
||||
`text(array, separator)` — Convert array to text. Elements are concatenated with the separator (default: empty text).
|
||||
|
||||
```javascript
|
||||
text(["h", "e", "l", "l", "o"]) // "hello"
|
||||
text(["a", "b", "c"], ", ") // "a, b, c"
|
||||
```
|
||||
|
||||
**From a number** — format as text:
|
||||
|
||||
`text(number, radix)` — Convert number to text. Radix is 2 thru 37 (default: 10).
|
||||
|
||||
`text(number, format)` — Format a number as text:
|
||||
|
||||
**Real styles:**
|
||||
|
||||
| Style | Description | Separator | Decimal |
|
||||
|-------|-------------|-----------|---------|
|
||||
| `"e"` | Scientific | | .period |
|
||||
| `"n"` | Number | | .period |
|
||||
| `"s"` | Space | space | .period |
|
||||
| `"u"` | Underbar | _underbar | .period |
|
||||
| `"d"` | Decimal | ,comma | .period |
|
||||
| `"c"` | Comma | .period | ,comma |
|
||||
| `"l"` | Locale | locale | locale |
|
||||
|
||||
**Integer styles:**
|
||||
|
||||
| Style | Base | Separator |
|
||||
|-------|------|-----------|
|
||||
| `"i"` | 10 | _underbar |
|
||||
| `"b"` | 2 | _underbar |
|
||||
| `"o"` | 8 | _underbar |
|
||||
| `"h"` | 16 | _underbar |
|
||||
| `"t"` | 32 | _underbar |
|
||||
|
||||
The format text is: `separation_digit` + `style_letter` + `places_digits`
|
||||
|
||||
```javascript
|
||||
var data = 123456789.1
|
||||
text(data) // "123456789.1"
|
||||
text(data, "3s4") // "123 456 789.1000"
|
||||
text(data, "d2") // "123,456,789.10"
|
||||
text(data, "e") // "1.234567891e8"
|
||||
text(data, "i") // "123456789"
|
||||
text(data, "8b") // "111_01011011_11001101_00010101"
|
||||
text(data, "h") // "75BCD15"
|
||||
text(12, "4b8") // "0000_1100"
|
||||
```
|
||||
|
||||
**From text** — extract a substring:
|
||||
|
||||
`text(text, from, to)` — Extract a substring. If from/to are negative, add length(text).
|
||||
|
||||
```javascript
|
||||
var my_text = "miskatonic"
|
||||
text(my_text, 0, 3) // "mis"
|
||||
text(my_text, 5) // "tonic"
|
||||
text(my_text, -3) // "nic"
|
||||
```
|
||||
|
||||
### record
|
||||
|
||||
The `record` function creates and manipulates records (objects). Its behavior depends on the type of the first argument:
|
||||
|
||||
**From a record** — copy, merge, or select:
|
||||
|
||||
`record(record)` — Copy. Make a mutable copy.
|
||||
|
||||
`record(record, another_record)` — Combine. Copy a record, then put all fields of another into the copy.
|
||||
|
||||
`record(record, array_of_keys)` — Select. New record with only the named fields.
|
||||
|
||||
**From an array of keys** — create a new record:
|
||||
|
||||
`record(array_of_keys)` — Set. New record using array as keys, each value is true.
|
||||
|
||||
`record(array_of_keys, value)` — Value Set. Each field value is value.
|
||||
|
||||
`record(array_of_keys, function)` — Functional Value Set. The function is called for each key to produce field values.
|
||||
|
||||
## Sensory Functions
|
||||
|
||||
The sensory functions always return a logical value. In ƿit, they use the `is_` prefix:
|
||||
|
||||
```javascript
|
||||
is_array([]) // true
|
||||
is_blob(blob.make()) // true
|
||||
is_function(x => x) // true
|
||||
is_integer(42) // true
|
||||
is_logical(true) // true
|
||||
is_null(null) // true
|
||||
is_number(3.14) // true
|
||||
is_object({}) // true
|
||||
is_text("hello") // true
|
||||
```
|
||||
|
||||
Additional type checks: `is_character`, `is_data`, `is_digit`, `is_false`, `is_fit`, `is_letter`, `is_lower`, `is_pattern`, `is_stone`, `is_true`, `is_upper`, `is_whitespace`.
|
||||
|
||||
## Standard Functions
|
||||
|
||||
### abs(number)
|
||||
|
||||
Absolute value. Returns null for non-numbers.
|
||||
|
||||
### apply(function, array)
|
||||
|
||||
Execute the function, passing the array elements as input values. If the array is longer than the function's arity, it disrupts.
|
||||
|
||||
### ceiling(number, place)
|
||||
|
||||
Round up. If place is 0 or null, round to the smallest integer >= number.
|
||||
|
||||
```javascript
|
||||
ceiling(12.3775) // 13
|
||||
ceiling(12.3775, -2) // 12.38
|
||||
ceiling(-12.3775) // -12
|
||||
```
|
||||
|
||||
### character(value)
|
||||
|
||||
If text, returns the first character. If a non-negative 32-bit integer, returns the character from that codepoint.
|
||||
|
||||
### codepoint(text)
|
||||
|
||||
Returns the codepoint number of the first character.
|
||||
|
||||
### ends_with(text, suffix)
|
||||
|
||||
Returns `true` if the text ends with the given suffix.
|
||||
|
||||
```javascript
|
||||
ends_with("hello.ce", ".ce") // true
|
||||
ends_with("hello.cm", ".ce") // false
|
||||
```
|
||||
|
||||
### every(array, function)
|
||||
|
||||
Returns `true` if every element satisfies the predicate.
|
||||
|
||||
```javascript
|
||||
every([2, 4, 6], x => x % 2 == 0) // true
|
||||
every([2, 3, 6], x => x % 2 == 0) // false
|
||||
```
|
||||
|
||||
### extract(text, pattern, from, to)
|
||||
|
||||
Match text to pattern. Returns a record of saved fields, or null if no match.
|
||||
|
||||
### fallback(requestor_array)
|
||||
|
||||
Returns a requestor that tries each requestor in order until one succeeds. Returns a cancel function. See [Requestors](/docs/requestors/) for usage.
|
||||
|
||||
### filter(array, function)
|
||||
|
||||
Call function for each element. When it returns true, the element is included in the new array.
|
||||
|
||||
```javascript
|
||||
filter([0, 1.25, 2, 3.5, 4, 5.75], is_integer) // [0, 2, 4]
|
||||
```
|
||||
|
||||
### find(array, function, reverse, from)
|
||||
|
||||
Call function for each element. If it returns true, return the element number. If the second argument is not a function, it is compared directly to elements. Returns null if not found.
|
||||
|
||||
```javascript
|
||||
find([1, 2, 3], x => x > 1) // 1
|
||||
find([1, 2, 3], 2) // 1
|
||||
```
|
||||
|
||||
### floor(number, place)
|
||||
|
||||
Round down. If place is 0 or null, round to the greatest integer <= number.
|
||||
|
||||
```javascript
|
||||
floor(12.3775) // 12
|
||||
floor(12.3775, -2) // 12.37
|
||||
floor(-12.3775) // -13
|
||||
```
|
||||
|
||||
### for(array, function, reverse, exit)
|
||||
|
||||
Call function with each element and its element number. If exit is not null and the function returns the exit value, return early.
|
||||
|
||||
### format(text, collection, transformer)
|
||||
|
||||
Substitute `{key}` placeholders in text with values from a collection (array or record).
|
||||
|
||||
```javascript
|
||||
format("{0} in {1}!", ["Malmborg", "Plano"])
|
||||
// "Malmborg in Plano!"
|
||||
```
|
||||
|
||||
### fraction(number)
|
||||
|
||||
Returns the fractional part of a number. See also `whole`.
|
||||
|
||||
### length(value)
|
||||
|
||||
| Value | Result |
|
||||
|-------|--------|
|
||||
| array | number of elements |
|
||||
| blob | number of bits |
|
||||
| text | number of codepoints |
|
||||
| function | number of named inputs (arity) |
|
||||
| record | record.length() |
|
||||
| other | null |
|
||||
|
||||
### lower(text)
|
||||
|
||||
Returns text with all uppercase characters converted to lowercase.
|
||||
|
||||
### max(number, number)
|
||||
|
||||
Returns the larger of two numbers. Returns null if either is not a number.
|
||||
|
||||
### min(number, number)
|
||||
|
||||
Returns the smaller of two numbers.
|
||||
|
||||
### modulo(dividend, divisor)
|
||||
|
||||
Result is `dividend - (divisor * floor(dividend / divisor))`. Result has the sign of the divisor.
|
||||
|
||||
### neg(number)
|
||||
|
||||
Negate. Reverse the sign of a number.
|
||||
|
||||
### normalize(text)
|
||||
|
||||
Unicode normalize.
|
||||
|
||||
### not(logical)
|
||||
|
||||
Returns the opposite logical. Returns null for non-logicals.
|
||||
|
||||
### parallel(requestor_array, throttle, need)
|
||||
|
||||
Returns a requestor that starts all requestors in the array. Results are collected into an array matching the input order. Optional throttle limits concurrent requestors. Optional need specifies the minimum number of successes required. See [Requestors](/docs/requestors/) for usage.
|
||||
|
||||
### print(value)
|
||||
|
||||
Print a value to standard output.
|
||||
|
||||
```javascript
|
||||
print("hello")
|
||||
print(42)
|
||||
print(`result: ${x}`)
|
||||
```
|
||||
|
||||
### race(requestor_array, throttle, need)
|
||||
|
||||
Like parallel, but returns as soon as the needed number of results are obtained. Default need is 1. Unfinished requestors are cancelled. See [Requestors](/docs/requestors/) for usage.
|
||||
|
||||
### reduce(array, function, initial, reverse)
|
||||
|
||||
Reduce an array to a single value by applying a function to pairs of elements.
|
||||
|
||||
```javascript
|
||||
reduce([1, 2, 3, 4, 5, 6, 7, 8, 9], (a, b) => a + b) // 45
|
||||
```
|
||||
|
||||
### remainder(dividend, divisor)
|
||||
|
||||
For fit integers: `dividend - ((dividend // divisor) * divisor)`.
|
||||
|
||||
### replace(text, target, replacement, limit)
|
||||
|
||||
Return text with target replaced by replacement. Target can be text or pattern. Replacement can be text or a function.
|
||||
|
||||
### reverse(array)
|
||||
|
||||
Returns a new array with elements in the opposite order.
|
||||
|
||||
### round(number, place)
|
||||
|
||||
Round to nearest.
|
||||
|
||||
```javascript
|
||||
round(12.3775) // 12
|
||||
round(12.3775, -2) // 12.38
|
||||
```
|
||||
|
||||
### search(text, target, from)
|
||||
|
||||
Search text for target. Returns character position or null.
|
||||
|
||||
### sequence(requestor_array)
|
||||
|
||||
Returns a requestor that processes each requestor in order. Each result becomes the input to the next. The last result is the final result. See [Requestors](/docs/requestors/) for usage.
|
||||
|
||||
### sign(number)
|
||||
|
||||
Returns -1, 0, or 1.
|
||||
|
||||
### some(array, function)
|
||||
|
||||
Returns `true` if any element satisfies the predicate. Stops at the first match.
|
||||
|
||||
```javascript
|
||||
some([1, 2, 3], x => x > 2) // true
|
||||
some([1, 2, 3], x => x > 5) // false
|
||||
```
|
||||
|
||||
### sort(array, select)
|
||||
|
||||
Returns a new sorted array. Sort keys must be all numbers or all texts. Sort is ascending and stable.
|
||||
|
||||
| select type | Sort key | Description |
|
||||
|-------------|----------|-------------|
|
||||
| null | element itself | Simple sort |
|
||||
| text | element[select] | Sort by record field |
|
||||
| number | element[select] | Sort by array index |
|
||||
| array | select[index] | External sort keys |
|
||||
|
||||
```javascript
|
||||
sort(["oats", "peas", "beans", "barley"])
|
||||
// ["barley", "beans", "oats", "peas"]
|
||||
|
||||
sort([{n: 3}, {n: 1}], "n")
|
||||
// [{n: 1}, {n: 3}]
|
||||
```
|
||||
|
||||
### starts_with(text, prefix)
|
||||
|
||||
Returns `true` if the text starts with the given prefix.
|
||||
|
||||
```javascript
|
||||
starts_with("hello world", "hello") // true
|
||||
starts_with("hello world", "world") // false
|
||||
```
|
||||
|
||||
### stone(value)
|
||||
|
||||
Petrify the value, making it permanently immutable. The operation is deep — all nested objects and arrays are also frozen. Returns the value.
|
||||
|
||||
### trim(text, reject)
|
||||
|
||||
Remove characters from both ends. Default removes whitespace.
|
||||
|
||||
### trunc(number, place)
|
||||
|
||||
Truncate toward zero.
|
||||
|
||||
```javascript
|
||||
trunc(12.3775) // 12
|
||||
trunc(-12.3775) // -12
|
||||
```
|
||||
|
||||
### upper(text)
|
||||
|
||||
Returns text with all lowercase characters converted to uppercase.
|
||||
|
||||
### whole(number)
|
||||
|
||||
Returns the whole part of a number. See also `fraction`.
|
||||
@@ -1,66 +0,0 @@
|
||||
# Cell
|
||||
|
||||

|
||||
|
||||
Cell is an actor-based scripting language for building concurrent applications. It combines a familiar JavaScript-like syntax with the actor model of computation.
|
||||
|
||||
## Key Features
|
||||
|
||||
- **Actor Model** — isolated memory, message passing, no shared state
|
||||
- **Immutability** — `stone()` makes values permanently frozen
|
||||
- **Prototype Inheritance** — objects without classes
|
||||
- **C Integration** — seamlessly extend with native code
|
||||
- **Cross-Platform** — deploy to desktop, web, and embedded
|
||||
|
||||
## Quick Start
|
||||
|
||||
```javascript
|
||||
// hello.ce - A simple actor
|
||||
log.console("Hello, Cell!")
|
||||
$stop()
|
||||
```
|
||||
|
||||
```bash
|
||||
cell hello
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
- [**Cell Language**](cellscript.md) — syntax, types, and built-in functions
|
||||
- [**Actors and Modules**](actors.md) — the execution model
|
||||
- [**Packages**](packages.md) — code organization and sharing
|
||||
- [**Command Line**](cli.md) — the `cell` tool
|
||||
- [**Writing C Modules**](c-modules.md) — native extensions
|
||||
|
||||
## Standard Library
|
||||
|
||||
- [text](library/text.md) — string manipulation
|
||||
- [number](library/number.md) — numeric operations
|
||||
- [array](library/array.md) — array utilities
|
||||
- [object](library/object.md) — object utilities
|
||||
- [blob](library/blob.md) — binary data
|
||||
- [time](library/time.md) — time and dates
|
||||
- [math](library/math.md) — trigonometry and math
|
||||
- [json](library/json.md) — JSON encoding/decoding
|
||||
- [random](library/random.md) — random numbers
|
||||
|
||||
## Architecture
|
||||
|
||||
Cell programs are organized into **packages**. Each package contains:
|
||||
|
||||
- **Modules** (`.cm`) — return a value, cached and frozen
|
||||
- **Actors** (`.ce`) — run independently, communicate via messages
|
||||
- **C files** (`.c`) — compiled to native libraries
|
||||
|
||||
Actors never share memory. They communicate by sending messages, which are automatically serialized. This makes concurrent programming safe and predictable.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
# Clone and bootstrap
|
||||
git clone https://gitea.pockle.world/john/cell
|
||||
cd cell
|
||||
make bootstrap
|
||||
```
|
||||
|
||||
The Cell shop is stored at `~/.cell/`.
|
||||
94
docs/kim.md
Normal file
94
docs/kim.md
Normal file
@@ -0,0 +1,94 @@
|
||||
---
|
||||
title: "Kim Encoding"
|
||||
description: "Compact character and count encoding"
|
||||
weight: 80
|
||||
type: "docs"
|
||||
---
|
||||
|
||||
Kim is a character and count encoding designed by Douglas Crockford. It encodes Unicode characters and variable-length integers using continuation bytes. Kim is simpler and more compact than UTF-8 for most text.
|
||||
|
||||
## Continuation Bytes
|
||||
|
||||
The fundamental idea in Kim is the continuation byte:
|
||||
|
||||
```
|
||||
C D D D D D D D
|
||||
```
|
||||
|
||||
- **C** — continue bit. If 1, read another byte. If 0, this is the last byte.
|
||||
- **D** (7 bits) — data bits.
|
||||
|
||||
To decode: shift the accumulator left by 7 bits, add the 7 data bits. If the continue bit is 1, repeat with the next byte. If 0, the value is complete.
|
||||
|
||||
To encode: take the value, emit 7 bits at a time from most significant to least significant, setting the continue bit on all bytes except the last.
|
||||
|
||||
## Character Encoding
|
||||
|
||||
Kim encodes Unicode codepoints directly as continuation byte sequences:
|
||||
|
||||
| Range | Bytes | Characters |
|
||||
|-------|-------|------------|
|
||||
| U+0000 to U+007F | 1 | ASCII |
|
||||
| U+0080 to U+3FFF | 2 | First quarter of BMP |
|
||||
| U+4000 to U+10FFFF | 3 | All other Unicode |
|
||||
|
||||
Unlike UTF-8, there is no need for surrogate pairs or escapement. Every Unicode character, including emoji and characters from extended planes, is encoded in at most 3 bytes.
|
||||
|
||||
### Examples
|
||||
|
||||
```
|
||||
'A' (U+0041) → 41
|
||||
'é' (U+00E9) → 81 69
|
||||
'💩' (U+1F4A9) → 87 E9 29
|
||||
```
|
||||
|
||||
## Count Encoding
|
||||
|
||||
Kim is also used for encoding counts (lengths, sizes). The same continuation byte format represents non-negative integers of arbitrary size:
|
||||
|
||||
| Range | Bytes |
|
||||
|-------|-------|
|
||||
| 0 to 127 | 1 |
|
||||
| 128 to 16383 | 2 |
|
||||
| 16384 to 2097151 | 3 |
|
||||
|
||||
## Comparison with UTF-8
|
||||
|
||||
| Property | Kim | UTF-8 |
|
||||
|----------|-----|-------|
|
||||
| ASCII | 1 byte | 1 byte |
|
||||
| BMP (first quarter) | 2 bytes | 2-3 bytes |
|
||||
| Full Unicode | 3 bytes | 3-4 bytes |
|
||||
| Self-synchronizing | No | Yes |
|
||||
| Sortable | No | Yes |
|
||||
| Simpler to implement | Yes | No |
|
||||
| Byte count for counts | Variable (7 bits/byte) | Not applicable |
|
||||
|
||||
Kim trades self-synchronization (the ability to find character boundaries from any position) for simplicity and compactness. In practice, Kim text is accessed sequentially, so self-synchronization is not needed.
|
||||
|
||||
## Usage in ƿit
|
||||
|
||||
Kim is used internally by blobs and by the Nota message format.
|
||||
|
||||
### In Blobs
|
||||
|
||||
The `blob.write_text` and `blob.read_text` functions use Kim to encode text into binary data:
|
||||
|
||||
```javascript
|
||||
var blob = use('blob')
|
||||
var b = blob.make()
|
||||
blob.write_text(b, "hello") // Kim-encoded length + characters
|
||||
stone(b)
|
||||
var text = blob.read_text(b, 0) // "hello"
|
||||
```
|
||||
|
||||
### In Nota
|
||||
|
||||
Nota uses Kim for two purposes:
|
||||
|
||||
1. **Counts** — array lengths, text lengths, blob sizes, record pair counts
|
||||
2. **Characters** — text content within Nota messages
|
||||
|
||||
The preamble byte of each Nota value incorporates the first few bits of a Kim-encoded count, with the continue bit indicating whether more bytes follow.
|
||||
|
||||
See [Nota Format](#nota) for the full specification.
|
||||
649
docs/language.md
Normal file
649
docs/language.md
Normal file
@@ -0,0 +1,649 @@
|
||||
---
|
||||
title: "ƿit Language"
|
||||
description: "Syntax, types, operators, and built-in functions"
|
||||
weight: 10
|
||||
type: "docs"
|
||||
---
|
||||
|
||||
ƿit is a scripting language for actor-based programming. It combines a familiar syntax with a prototype-based object system and strict immutability semantics.
|
||||
|
||||
## Basics
|
||||
|
||||
### Variables and Constants
|
||||
|
||||
Variables are declared with `var`, constants with `def`. All declarations must be initialized and must appear at the function body level — not inside `if`, `while`, `for`, or bare `{}` blocks.
|
||||
|
||||
```javascript
|
||||
var x = 10
|
||||
var name = "pit"
|
||||
var empty = null
|
||||
|
||||
def PI = 3.14159 // constant, cannot be reassigned
|
||||
|
||||
var a = 1, b = 2, c = 3 // multiple declarations
|
||||
```
|
||||
|
||||
### Data Types
|
||||
|
||||
ƿit has eight fundamental types:
|
||||
|
||||
- **number** — DEC64 decimal floating point (no rounding errors)
|
||||
- **text** — Unicode strings
|
||||
- **logical** — `true` or `false`
|
||||
- **null** — the absence of a value (no `undefined`)
|
||||
- **array** — ordered, numerically-indexed sequences
|
||||
- **object** — key-value records with prototype inheritance
|
||||
- **blob** — binary data (bits, not bytes)
|
||||
- **function** — first-class callable values
|
||||
|
||||
### Literals
|
||||
|
||||
```javascript
|
||||
// Numbers
|
||||
42
|
||||
3.14
|
||||
-5
|
||||
0
|
||||
1e3 // scientific notation (1000)
|
||||
|
||||
// Text
|
||||
"hello"
|
||||
`template ${x}` // string interpolation
|
||||
`${1 + 2}` // expression interpolation
|
||||
|
||||
// Logical
|
||||
true
|
||||
false
|
||||
|
||||
// Null
|
||||
null
|
||||
|
||||
// Arrays
|
||||
[1, 2, 3]
|
||||
[]
|
||||
|
||||
// Objects
|
||||
{a: 1, b: "two"}
|
||||
{}
|
||||
|
||||
// Regex
|
||||
/\d+/
|
||||
/hello/i // with flags
|
||||
```
|
||||
|
||||
## Operators
|
||||
|
||||
### Arithmetic
|
||||
|
||||
```javascript
|
||||
2 + 3 // 5
|
||||
5 - 3 // 2
|
||||
3 * 4 // 12
|
||||
12 / 4 // 3
|
||||
10 % 3 // 1
|
||||
2 ** 3 // 8 (exponentiation)
|
||||
```
|
||||
|
||||
### Comparison
|
||||
|
||||
All comparisons are strict — there is no type coercion.
|
||||
|
||||
```javascript
|
||||
5 == 5 // true
|
||||
5 != 6 // true
|
||||
3 < 5 // true
|
||||
5 > 3 // true
|
||||
3 <= 3 // true
|
||||
5 >= 5 // true
|
||||
```
|
||||
|
||||
### Logical
|
||||
|
||||
```javascript
|
||||
true && true // true
|
||||
true && false // false
|
||||
false || true // true
|
||||
false || false // false
|
||||
!true // false
|
||||
!false // true
|
||||
```
|
||||
|
||||
Logical operators short-circuit:
|
||||
|
||||
```javascript
|
||||
var called = false
|
||||
var fn = function() { called = true; return true }
|
||||
var r = false && fn() // fn() not called
|
||||
r = true || fn() // fn() not called
|
||||
```
|
||||
|
||||
### Bitwise
|
||||
|
||||
```javascript
|
||||
5 & 3 // 1 (AND)
|
||||
5 | 3 // 7 (OR)
|
||||
5 ^ 3 // 6 (XOR)
|
||||
~0 // -1 (NOT)
|
||||
1 << 3 // 8 (left shift)
|
||||
8 >> 3 // 1 (right shift)
|
||||
-1 >>> 1 // 2147483647 (unsigned right shift)
|
||||
```
|
||||
|
||||
### Unary
|
||||
|
||||
```javascript
|
||||
+5 // 5
|
||||
-5 // -5
|
||||
-(-5) // 5
|
||||
```
|
||||
|
||||
### Increment and Decrement
|
||||
|
||||
```javascript
|
||||
var x = 5
|
||||
x++ // returns 5, x becomes 6 (postfix)
|
||||
++x // returns 7, x becomes 7 (prefix)
|
||||
x-- // returns 7, x becomes 6 (postfix)
|
||||
--x // returns 5, x becomes 5 (prefix)
|
||||
```
|
||||
|
||||
### Compound Assignment
|
||||
|
||||
```javascript
|
||||
var x = 10
|
||||
x += 3 // 13
|
||||
x -= 3 // 10
|
||||
x *= 2 // 20
|
||||
x /= 4 // 5
|
||||
x %= 3 // 2
|
||||
```
|
||||
|
||||
### Ternary
|
||||
|
||||
```javascript
|
||||
var a = true ? 1 : 2 // 1
|
||||
var b = false ? 1 : 2 // 2
|
||||
var c = true ? (false ? 1 : 2) : 3 // 2 (nested)
|
||||
```
|
||||
|
||||
### Comma
|
||||
|
||||
The comma operator evaluates all expressions and returns the last.
|
||||
|
||||
```javascript
|
||||
var x = (1, 2, 3) // 3
|
||||
```
|
||||
|
||||
### In
|
||||
|
||||
Test whether a key exists in an object.
|
||||
|
||||
```javascript
|
||||
var o = {a: 1}
|
||||
"a" in o // true
|
||||
"b" in o // false
|
||||
```
|
||||
|
||||
### Delete
|
||||
|
||||
Remove a key from an object.
|
||||
|
||||
```javascript
|
||||
var o = {a: 1, b: 2}
|
||||
delete o.a
|
||||
"a" in o // false
|
||||
o.b // 2
|
||||
```
|
||||
|
||||
## Property Access
|
||||
|
||||
### Dot and Bracket
|
||||
|
||||
```javascript
|
||||
var o = {x: 10}
|
||||
o.x // 10 (dot read)
|
||||
o.x = 20 // dot write
|
||||
o["x"] // 20 (bracket read)
|
||||
var key = "x"
|
||||
o[key] // 20 (computed bracket)
|
||||
o["y"] = 30 // bracket write
|
||||
```
|
||||
|
||||
### Object as Key
|
||||
|
||||
Objects can be used as keys in other objects.
|
||||
|
||||
```javascript
|
||||
var k = {}
|
||||
var o = {}
|
||||
o[k] = 42
|
||||
o[k] // 42
|
||||
o[{}] // null (different object)
|
||||
k in o // true
|
||||
delete o[k]
|
||||
k in o // false
|
||||
```
|
||||
|
||||
### Chained Access
|
||||
|
||||
```javascript
|
||||
var d = {a: {b: [1, {c: 99}]}}
|
||||
d.a.b[1].c // 99
|
||||
```
|
||||
|
||||
## Arrays
|
||||
|
||||
Arrays are **distinct from objects**. They are ordered, numerically-indexed sequences.
|
||||
|
||||
```javascript
|
||||
var arr = [1, 2, 3]
|
||||
arr[0] // 1
|
||||
arr[2] = 10 // [1, 2, 10]
|
||||
length(arr) // 3
|
||||
```
|
||||
|
||||
### Push and Pop
|
||||
|
||||
```javascript
|
||||
var a = [1, 2]
|
||||
a[] = 3 // push: [1, 2, 3]
|
||||
length(a) // 3
|
||||
var v = a[] // pop: v is 3, a is [1, 2]
|
||||
length(a) // 2
|
||||
```
|
||||
|
||||
## Objects
|
||||
|
||||
Objects are key-value records with prototype-based inheritance.
|
||||
|
||||
```javascript
|
||||
var point = {x: 10, y: 20}
|
||||
point.x // 10
|
||||
point["y"] // 20
|
||||
```
|
||||
|
||||
### Prototypes
|
||||
|
||||
```javascript
|
||||
// Create object with prototype
|
||||
var parent = {x: 10}
|
||||
var child = meme(parent)
|
||||
child.x // 10 (inherited)
|
||||
proto(child) // parent
|
||||
|
||||
// Override does not mutate parent
|
||||
child.x = 20
|
||||
parent.x // 10
|
||||
```
|
||||
|
||||
### Mixins
|
||||
|
||||
```javascript
|
||||
var p = {a: 1}
|
||||
var m1 = {b: 2}
|
||||
var m2 = {c: 3}
|
||||
var child = meme(p, [m1, m2])
|
||||
child.a // 1 (from prototype)
|
||||
child.b // 2 (from mixin)
|
||||
child.c // 3 (from mixin)
|
||||
```
|
||||
|
||||
## Control Flow
|
||||
|
||||
### If / Else
|
||||
|
||||
```javascript
|
||||
var x = 0
|
||||
if (true) x = 1
|
||||
if (false) x = 2 else x = 3
|
||||
if (false) x = 4
|
||||
else if (true) x = 5
|
||||
else x = 6
|
||||
```
|
||||
|
||||
### While
|
||||
|
||||
```javascript
|
||||
var i = 0
|
||||
while (i < 5) i++
|
||||
|
||||
// break
|
||||
i = 0
|
||||
while (true) {
|
||||
if (i >= 3) break
|
||||
i++
|
||||
}
|
||||
|
||||
// continue
|
||||
var sum = 0
|
||||
i = 0
|
||||
while (i < 5) {
|
||||
i++
|
||||
if (i % 2 == 0) continue
|
||||
sum += i
|
||||
}
|
||||
```
|
||||
|
||||
### For
|
||||
|
||||
Variables cannot be declared in the for initializer. Declare them at the function body level.
|
||||
|
||||
```javascript
|
||||
var sum = 0
|
||||
var i = 0
|
||||
for (i = 0; i < 5; i++) sum += i
|
||||
|
||||
// break
|
||||
sum = 0
|
||||
i = 0
|
||||
for (i = 0; i < 10; i++) {
|
||||
if (i == 5) break
|
||||
sum += i
|
||||
}
|
||||
|
||||
// continue
|
||||
sum = 0
|
||||
i = 0
|
||||
for (i = 0; i < 5; i++) {
|
||||
if (i % 2 == 0) continue
|
||||
sum += i
|
||||
}
|
||||
|
||||
// nested
|
||||
sum = 0
|
||||
var j = 0
|
||||
for (i = 0; i < 3; i++) {
|
||||
for (j = 0; j < 3; j++) {
|
||||
sum++
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Functions
|
||||
|
||||
### Function Expressions
|
||||
|
||||
```javascript
|
||||
var add = function(a, b) { return a + b }
|
||||
add(2, 3) // 5
|
||||
```
|
||||
|
||||
### Arrow Functions
|
||||
|
||||
```javascript
|
||||
var double = x => x * 2
|
||||
double(5) // 10
|
||||
|
||||
var sum = (a, b) => a + b
|
||||
sum(2, 3) // 5
|
||||
|
||||
var block = x => {
|
||||
var y = x * 2
|
||||
return y + 1
|
||||
}
|
||||
block(5) // 11
|
||||
```
|
||||
|
||||
### Return
|
||||
|
||||
A function with no `return` returns `null`. An early `return` exits immediately.
|
||||
|
||||
```javascript
|
||||
var fn = function() { var x = 1 }
|
||||
fn() // null
|
||||
|
||||
var fn2 = function() { return 1; return 2 }
|
||||
fn2() // 1
|
||||
```
|
||||
|
||||
### Arguments
|
||||
|
||||
Extra arguments are ignored. Missing arguments are `null`.
|
||||
|
||||
```javascript
|
||||
var fn = function(a, b) { return a + b }
|
||||
fn(1, 2, 3) // 3 (extra arg ignored)
|
||||
|
||||
var fn2 = function(a, b) { return a }
|
||||
fn2(1) // 1 (b is null)
|
||||
```
|
||||
|
||||
### Immediately Invoked Function Expression
|
||||
|
||||
```javascript
|
||||
var r = (function(x) { return x * 2 })(21) // 42
|
||||
```
|
||||
|
||||
### Closures
|
||||
|
||||
Functions capture variables from their enclosing scope.
|
||||
|
||||
```javascript
|
||||
var make = function(x) {
|
||||
return function(y) { return x + y }
|
||||
}
|
||||
var add5 = make(5)
|
||||
add5(3) // 8
|
||||
```
|
||||
|
||||
Captured variables can be mutated:
|
||||
|
||||
```javascript
|
||||
var counter = function() {
|
||||
var n = 0
|
||||
return function() { n = n + 1; return n }
|
||||
}
|
||||
var c = counter()
|
||||
c() // 1
|
||||
c() // 2
|
||||
```
|
||||
|
||||
### Recursion
|
||||
|
||||
```javascript
|
||||
var fact = function(n) {
|
||||
if (n <= 1) return 1
|
||||
return n * fact(n - 1)
|
||||
}
|
||||
fact(5) // 120
|
||||
```
|
||||
|
||||
### This Binding
|
||||
|
||||
When a function is called as a method, `this` refers to the object.
|
||||
|
||||
```javascript
|
||||
var obj = {
|
||||
val: 10,
|
||||
get: function() { return this.val }
|
||||
}
|
||||
obj.get() // 10
|
||||
```
|
||||
|
||||
### Currying
|
||||
|
||||
```javascript
|
||||
var f = function(a) {
|
||||
return function(b) {
|
||||
return function(c) { return a + b + c }
|
||||
}
|
||||
}
|
||||
f(1)(2)(3) // 6
|
||||
```
|
||||
|
||||
## Identifiers
|
||||
|
||||
Identifiers can contain `?` and `!` characters, both as suffixes and mid-name.
|
||||
|
||||
```javascript
|
||||
var nil? = (x) => x == null
|
||||
nil?(null) // true
|
||||
nil?(42) // false
|
||||
|
||||
var set! = (x) => x + 1
|
||||
set!(5) // 6
|
||||
|
||||
var is?valid = (x) => x > 0
|
||||
is?valid(3) // true
|
||||
|
||||
var do!stuff = () => 42
|
||||
do!stuff() // 42
|
||||
```
|
||||
|
||||
The `?` in an identifier is not confused with the ternary operator:
|
||||
|
||||
```javascript
|
||||
var nil? = (x) => x == null
|
||||
var a = nil?(null) ? "yes" : "no" // "yes"
|
||||
```
|
||||
|
||||
## Type Checking
|
||||
|
||||
### Type Functions
|
||||
|
||||
```javascript
|
||||
is_number(42) // true
|
||||
is_text("hi") // true
|
||||
is_logical(true) // true
|
||||
is_object({}) // true
|
||||
is_array([]) // true
|
||||
is_function(function(){}) // true
|
||||
is_null(null) // true
|
||||
is_object([]) // false (array is not object)
|
||||
is_array({}) // false (object is not array)
|
||||
```
|
||||
|
||||
### Truthiness
|
||||
|
||||
Falsy values: `false`, `0`, `""`, `null`. Everything else is truthy.
|
||||
|
||||
```javascript
|
||||
if (0) ... // not entered
|
||||
if ("") ... // not entered
|
||||
if (null) ... // not entered
|
||||
if (1) ... // entered
|
||||
if ("hi") ... // entered
|
||||
if ({}) ... // entered
|
||||
if ([]) ... // entered
|
||||
```
|
||||
|
||||
## Immutability with Stone
|
||||
|
||||
The `stone()` function makes values permanently immutable.
|
||||
|
||||
```javascript
|
||||
var o = {x: 1}
|
||||
is_stone(o) // false
|
||||
stone(o)
|
||||
is_stone(o) // true
|
||||
o.x = 2 // disrupts!
|
||||
```
|
||||
|
||||
Stone is **deep** — all nested objects and arrays are also frozen. This cannot be reversed.
|
||||
|
||||
## Function Proxy
|
||||
|
||||
A function with two parameters (`name`, `args`) acts as a proxy when properties are accessed on it. Any method call on the function dispatches through the proxy.
|
||||
|
||||
```javascript
|
||||
var proxy = function(name, args) {
|
||||
return `${name}:${length(args)}`
|
||||
}
|
||||
proxy.hello() // "hello:0"
|
||||
proxy.add(1, 2) // "add:2"
|
||||
proxy["method"]() // "method:0"
|
||||
var m = "dynamic"
|
||||
proxy[m]() // "dynamic:0"
|
||||
```
|
||||
|
||||
For non-proxy functions, property access disrupts:
|
||||
|
||||
```javascript
|
||||
var fn = function() { return 1 }
|
||||
fn.foo // disrupts
|
||||
fn.foo = 1 // disrupts
|
||||
```
|
||||
|
||||
## Regex
|
||||
|
||||
Regex literals are written with forward slashes, with optional flags.
|
||||
|
||||
```javascript
|
||||
var r = /\d+/
|
||||
var result = extract("abc123", r)
|
||||
result[0] // "123"
|
||||
|
||||
var ri = /hello/i
|
||||
var result2 = extract("Hello", ri)
|
||||
result2[0] // "Hello"
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
ƿit uses `disrupt` and `disruption` for error handling. A `disrupt` signals that something went wrong. The `disruption` block attached to a function catches it.
|
||||
|
||||
```javascript
|
||||
var safe_divide = function(a, b) {
|
||||
if (b == 0) disrupt
|
||||
return a / b
|
||||
} disruption {
|
||||
print("something went wrong")
|
||||
}
|
||||
```
|
||||
|
||||
`disrupt` is a bare keyword — it does not carry a value. The `disruption` block knows that something went wrong, but not what.
|
||||
|
||||
### Re-raising
|
||||
|
||||
A `disruption` block can re-raise by calling `disrupt` again:
|
||||
|
||||
```javascript
|
||||
var outer = function() {
|
||||
var inner = function() { disrupt } disruption { disrupt }
|
||||
inner()
|
||||
} disruption {
|
||||
// caught here after re-raise
|
||||
}
|
||||
outer()
|
||||
```
|
||||
|
||||
### Testing for Disruption
|
||||
|
||||
```javascript
|
||||
var should_disrupt = function(fn) {
|
||||
var caught = false
|
||||
var wrapper = function() {
|
||||
fn()
|
||||
} disruption {
|
||||
caught = true
|
||||
}
|
||||
wrapper()
|
||||
return caught
|
||||
}
|
||||
```
|
||||
|
||||
If an actor has an unhandled disruption, it crashes.
|
||||
|
||||
## Self-Referencing Structures
|
||||
|
||||
Objects can reference themselves:
|
||||
|
||||
```javascript
|
||||
var o = {name: "root"}
|
||||
o.self = o
|
||||
o.self.self.name // "root"
|
||||
```
|
||||
|
||||
## Variable Shadowing
|
||||
|
||||
Inner functions can shadow outer variables:
|
||||
|
||||
```javascript
|
||||
var x = 10
|
||||
var fn = function() {
|
||||
var x = 20
|
||||
return x
|
||||
}
|
||||
fn() // 20
|
||||
x // 10
|
||||
```
|
||||
@@ -1,10 +0,0 @@
|
||||
nav:
|
||||
- text.md
|
||||
- number.md
|
||||
- array.md
|
||||
- object.md
|
||||
- blob.md
|
||||
- time.md
|
||||
- math.md
|
||||
- json.md
|
||||
- random.md
|
||||
18
docs/library/_index.md
Normal file
18
docs/library/_index.md
Normal file
@@ -0,0 +1,18 @@
|
||||
---
|
||||
title: "Standard Library"
|
||||
description: "ƿit standard library modules"
|
||||
weight: 90
|
||||
type: "docs"
|
||||
---
|
||||
|
||||
The standard library provides modules loaded with `use()`.
|
||||
|
||||
| Module | Description |
|
||||
|--------|-------------|
|
||||
| [blob](/docs/library/blob/) | Binary data (bits, not bytes) |
|
||||
| [time](/docs/library/time/) | Time constants and conversions |
|
||||
| [math](/docs/library/math/) | Trigonometry, logarithms, roots |
|
||||
| [json](/docs/library/json/) | JSON encoding and decoding |
|
||||
| [random](/docs/library/random/) | Random number generation |
|
||||
|
||||
The `text`, `number`, `array`, and `object` functions are intrinsics — they are always available without `use`. See [Built-in Functions](/docs/functions/) for the full list, and the individual reference pages for [text](/docs/library/text/), [number](/docs/library/number/), [array](/docs/library/array/), and [object](/docs/library/object/).
|
||||
@@ -1,12 +1,19 @@
|
||||
# array
|
||||
---
|
||||
title: "array"
|
||||
description: "Array creation and manipulation"
|
||||
weight: 30
|
||||
type: "docs"
|
||||
---
|
||||
|
||||
The `array` function and its methods handle array creation and manipulation.
|
||||
The `array` function is an intrinsic (always available, no `use()` needed). It is **polymorphic** — its behavior depends on the type of the first argument.
|
||||
|
||||
## Creation
|
||||
## From a Number
|
||||
|
||||
Create an array of a given size.
|
||||
|
||||
### array(number)
|
||||
|
||||
Create an array of specified size, filled with `null`.
|
||||
All elements initialized to `null`.
|
||||
|
||||
```javascript
|
||||
array(3) // [null, null, null]
|
||||
@@ -14,24 +21,36 @@ array(3) // [null, null, null]
|
||||
|
||||
### array(number, initial)
|
||||
|
||||
Create an array with initial values.
|
||||
All elements initialized to a value. If initial is a function, it is called for each element (passed the index if arity >= 1).
|
||||
|
||||
```javascript
|
||||
array(3, 0) // [0, 0, 0]
|
||||
array(3, i => i * 2) // [0, 2, 4]
|
||||
```
|
||||
|
||||
## From an Array
|
||||
|
||||
Copy, map, concat, or slice.
|
||||
|
||||
### array(array)
|
||||
|
||||
Copy an array.
|
||||
Copy an array (mutable).
|
||||
|
||||
```javascript
|
||||
var copy = array(original)
|
||||
```
|
||||
|
||||
### array(array, function)
|
||||
|
||||
Map — call function with each element, collect results.
|
||||
|
||||
```javascript
|
||||
array([1, 2, 3], x => x * 2) // [2, 4, 6]
|
||||
```
|
||||
|
||||
### array(array, from, to)
|
||||
|
||||
Slice an array.
|
||||
Slice — extract a sub-array. Negative indices count from end.
|
||||
|
||||
```javascript
|
||||
array([1, 2, 3, 4, 5], 1, 4) // [2, 3, 4]
|
||||
@@ -40,32 +59,36 @@ array([1, 2, 3], -2) // [2, 3]
|
||||
|
||||
### array(array, another)
|
||||
|
||||
Concatenate arrays.
|
||||
Concatenate two arrays.
|
||||
|
||||
```javascript
|
||||
array([1, 2], [3, 4]) // [1, 2, 3, 4]
|
||||
```
|
||||
|
||||
### array(object)
|
||||
## From a Record
|
||||
|
||||
Get keys of an object.
|
||||
### array(record)
|
||||
|
||||
Get the keys of a record as an array of text.
|
||||
|
||||
```javascript
|
||||
array({a: 1, b: 2}) // ["a", "b"]
|
||||
```
|
||||
|
||||
## From Text
|
||||
|
||||
### array(text)
|
||||
|
||||
Split text into grapheme clusters.
|
||||
Split text into individual characters (grapheme clusters). This is the standard way to iterate over characters in a string.
|
||||
|
||||
```javascript
|
||||
array("hello") // ["h", "e", "l", "l", "o"]
|
||||
array("👨👩👧") // ["👨👩👧"]
|
||||
array("hello") // ["h", "e", "l", "l", "o"]
|
||||
array("ƿit") // ["ƿ", "i", "t"]
|
||||
```
|
||||
|
||||
### array(text, separator)
|
||||
|
||||
Split text by separator.
|
||||
Split text by a separator string.
|
||||
|
||||
```javascript
|
||||
array("a,b,c", ",") // ["a", "b", "c"]
|
||||
@@ -73,7 +96,7 @@ array("a,b,c", ",") // ["a", "b", "c"]
|
||||
|
||||
### array(text, length)
|
||||
|
||||
Split text into chunks.
|
||||
Dice text into chunks of a given length.
|
||||
|
||||
```javascript
|
||||
array("abcdef", 2) // ["ab", "cd", "ef"]
|
||||
@@ -87,13 +110,13 @@ Iterate over elements.
|
||||
|
||||
```javascript
|
||||
array.for([1, 2, 3], function(el, i) {
|
||||
log.console(i, el)
|
||||
print(i, el)
|
||||
})
|
||||
|
||||
// With early exit
|
||||
array.for([1, 2, 3, 4], function(el) {
|
||||
if (el > 2) return true
|
||||
log.console(el)
|
||||
print(el)
|
||||
}, false, true) // prints 1, 2
|
||||
```
|
||||
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
# blob
|
||||
---
|
||||
title: "blob"
|
||||
description: "Binary data containers (bits, not bytes)"
|
||||
weight: 50
|
||||
type: "docs"
|
||||
---
|
||||
|
||||
Blobs are binary large objects — containers of bits (not bytes). They're used for encoding data, messages, images, network payloads, and more.
|
||||
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
# json
|
||||
---
|
||||
title: "json"
|
||||
description: "JSON encoding and decoding"
|
||||
weight: 80
|
||||
type: "docs"
|
||||
---
|
||||
|
||||
JSON encoding and decoding.
|
||||
|
||||
@@ -86,5 +91,5 @@ var config_text = json.encode(config, 2)
|
||||
|
||||
// Load configuration
|
||||
var loaded = json.decode(config_text)
|
||||
log.console(loaded.debug) // true
|
||||
print(loaded.debug) // true
|
||||
```
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
# math
|
||||
---
|
||||
title: "math"
|
||||
description: "Trigonometry, logarithms, and roots"
|
||||
weight: 70
|
||||
type: "docs"
|
||||
---
|
||||
|
||||
Cell provides three math modules with identical functions but different angle representations:
|
||||
ƿit provides three math modules with identical functions but different angle representations:
|
||||
|
||||
```javascript
|
||||
var math = use('math/radians') // angles in radians
|
||||
var math = use('math/degrees') // angles in degrees
|
||||
var math = use('math/degrees') // angles in degrees
|
||||
var math = use('math/cycles') // angles in cycles (0-1)
|
||||
```
|
||||
|
||||
@@ -35,7 +40,7 @@ math.tangent(math.pi / 4) // 1 (radians)
|
||||
Inverse sine.
|
||||
|
||||
```javascript
|
||||
math.arc_sine(1) // π/2 (radians)
|
||||
math.arc_sine(1) // pi/2 (radians)
|
||||
```
|
||||
|
||||
### arc_cosine(n)
|
||||
@@ -43,7 +48,7 @@ math.arc_sine(1) // π/2 (radians)
|
||||
Inverse cosine.
|
||||
|
||||
```javascript
|
||||
math.arc_cosine(0) // π/2 (radians)
|
||||
math.arc_cosine(0) // pi/2 (radians)
|
||||
```
|
||||
|
||||
### arc_tangent(n, denominator)
|
||||
@@ -51,9 +56,9 @@ math.arc_cosine(0) // π/2 (radians)
|
||||
Inverse tangent. With two arguments, computes atan2.
|
||||
|
||||
```javascript
|
||||
math.arc_tangent(1) // π/4 (radians)
|
||||
math.arc_tangent(1, 1) // π/4 (radians)
|
||||
math.arc_tangent(-1, -1) // -3π/4 (radians)
|
||||
math.arc_tangent(1) // pi/4 (radians)
|
||||
math.arc_tangent(1, 1) // pi/4 (radians)
|
||||
math.arc_tangent(-1, -1) // -3pi/4 (radians)
|
||||
```
|
||||
|
||||
## Exponentials and Logarithms
|
||||
@@ -64,7 +69,7 @@ Euler's number raised to a power. Default power is 1.
|
||||
|
||||
```javascript
|
||||
math.e() // 2.718281828...
|
||||
math.e(2) // e²
|
||||
math.e(2) // e^2
|
||||
```
|
||||
|
||||
### ln(n)
|
||||
@@ -130,21 +135,21 @@ math.e() // 2.71828...
|
||||
var math = use('math/radians')
|
||||
|
||||
// Distance between two points
|
||||
function distance(x1, y1, x2, y2) {
|
||||
var distance = function(x1, y1, x2, y2) {
|
||||
var dx = x2 - x1
|
||||
var dy = y2 - y1
|
||||
return math.sqrt(dx * dx + dy * dy)
|
||||
}
|
||||
|
||||
// Angle between two points
|
||||
function angle(x1, y1, x2, y2) {
|
||||
var angle = function(x1, y1, x2, y2) {
|
||||
return math.arc_tangent(y2 - y1, x2 - x1)
|
||||
}
|
||||
|
||||
// Rotate a point
|
||||
function rotate(x, y, angle) {
|
||||
var c = math.cosine(angle)
|
||||
var s = math.sine(angle)
|
||||
var rotate = function(x, y, a) {
|
||||
var c = math.cosine(a)
|
||||
var s = math.sine(a)
|
||||
return {
|
||||
x: x * c - y * s,
|
||||
y: x * s + y * c
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
# number
|
||||
---
|
||||
title: "number"
|
||||
description: "Numeric conversion and operations"
|
||||
weight: 20
|
||||
type: "docs"
|
||||
---
|
||||
|
||||
The `number` function and its methods handle numeric conversion and operations.
|
||||
The `number` function is an intrinsic (always available, no `use()` needed). It is **polymorphic** — its behavior depends on the type of the first argument.
|
||||
|
||||
## Conversion
|
||||
|
||||
@@ -29,15 +34,15 @@ Parse formatted numbers.
|
||||
|
||||
| Format | Description |
|
||||
|--------|-------------|
|
||||
| `""` | Standard decimal |
|
||||
| `"u"` | Underbar separator (1_000) |
|
||||
| `"d"` | Comma separator (1,000) |
|
||||
| `"s"` | Space separator (1 000) |
|
||||
| `"v"` | European (1.000,50) |
|
||||
| `"b"` | Binary |
|
||||
| `"o"` | Octal |
|
||||
| `"h"` | Hexadecimal |
|
||||
| `"j"` | JavaScript style (0x, 0o, 0b prefixes) |
|
||||
| `""` | Standard decimal |
|
||||
| `"u"` | Underbar separator (1_000) |
|
||||
| `"d"` | Comma separator (1,000) |
|
||||
| `"s"` | Space separator (1 000) |
|
||||
| `"v"` | European (1.000,50) |
|
||||
| `"b"` | Binary |
|
||||
| `"o"` | Octal |
|
||||
| `"h"` | Hexadecimal |
|
||||
| `"j"` | JavaScript style (0x, 0o, 0b prefixes) |
|
||||
|
||||
```javascript
|
||||
number("1,000", "d") // 1000
|
||||
@@ -46,98 +51,98 @@ number("0xff", "j") // 255
|
||||
|
||||
## Methods
|
||||
|
||||
### number.abs(n)
|
||||
### abs(n)
|
||||
|
||||
Absolute value.
|
||||
|
||||
```javascript
|
||||
number.abs(-5) // 5
|
||||
number.abs(5) // 5
|
||||
abs(-5) // 5
|
||||
abs(5) // 5
|
||||
```
|
||||
|
||||
### number.sign(n)
|
||||
### sign(n)
|
||||
|
||||
Returns -1, 0, or 1.
|
||||
|
||||
```javascript
|
||||
number.sign(-5) // -1
|
||||
number.sign(0) // 0
|
||||
number.sign(5) // 1
|
||||
sign(-5) // -1
|
||||
sign(0) // 0
|
||||
sign(5) // 1
|
||||
```
|
||||
|
||||
### number.floor(n, place)
|
||||
### floor(n, place)
|
||||
|
||||
Round down.
|
||||
|
||||
```javascript
|
||||
number.floor(4.9) // 4
|
||||
number.floor(4.567, 2) // 4.56
|
||||
floor(4.9) // 4
|
||||
floor(4.567, 2) // 4.56
|
||||
```
|
||||
|
||||
### number.ceiling(n, place)
|
||||
### ceiling(n, place)
|
||||
|
||||
Round up.
|
||||
|
||||
```javascript
|
||||
number.ceiling(4.1) // 5
|
||||
number.ceiling(4.123, 2) // 4.13
|
||||
ceiling(4.1) // 5
|
||||
ceiling(4.123, 2) // 4.13
|
||||
```
|
||||
|
||||
### number.round(n, place)
|
||||
### round(n, place)
|
||||
|
||||
Round to nearest.
|
||||
|
||||
```javascript
|
||||
number.round(4.5) // 5
|
||||
number.round(4.567, 2) // 4.57
|
||||
round(4.5) // 5
|
||||
round(4.567, 2) // 4.57
|
||||
```
|
||||
|
||||
### number.trunc(n, place)
|
||||
### trunc(n, place)
|
||||
|
||||
Truncate toward zero.
|
||||
|
||||
```javascript
|
||||
number.trunc(4.9) // 4
|
||||
number.trunc(-4.9) // -4
|
||||
trunc(4.9) // 4
|
||||
trunc(-4.9) // -4
|
||||
```
|
||||
|
||||
### number.whole(n)
|
||||
### whole(n)
|
||||
|
||||
Get the integer part.
|
||||
|
||||
```javascript
|
||||
number.whole(4.9) // 4
|
||||
number.whole(-4.9) // -4
|
||||
whole(4.9) // 4
|
||||
whole(-4.9) // -4
|
||||
```
|
||||
|
||||
### number.fraction(n)
|
||||
### fraction(n)
|
||||
|
||||
Get the fractional part.
|
||||
|
||||
```javascript
|
||||
number.fraction(4.75) // 0.75
|
||||
fraction(4.75) // 0.75
|
||||
```
|
||||
|
||||
### number.min(...values)
|
||||
### min(a, b)
|
||||
|
||||
Return the smallest value.
|
||||
Return the smaller of two numbers.
|
||||
|
||||
```javascript
|
||||
number.min(3, 1, 4, 1, 5) // 1
|
||||
min(3, 5) // 3
|
||||
```
|
||||
|
||||
### number.max(...values)
|
||||
### max(a, b)
|
||||
|
||||
Return the largest value.
|
||||
Return the larger of two numbers.
|
||||
|
||||
```javascript
|
||||
number.max(3, 1, 4, 1, 5) // 5
|
||||
max(3, 5) // 5
|
||||
```
|
||||
|
||||
### number.remainder(dividend, divisor)
|
||||
### remainder(dividend, divisor)
|
||||
|
||||
Compute remainder.
|
||||
|
||||
```javascript
|
||||
number.remainder(17, 5) // 2
|
||||
remainder(17, 5) // 2
|
||||
```
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
# object
|
||||
---
|
||||
title: "object"
|
||||
description: "Object creation and manipulation"
|
||||
weight: 40
|
||||
type: "docs"
|
||||
---
|
||||
|
||||
The `object` function and related utilities handle object creation and manipulation.
|
||||
The `object` function is an intrinsic (always available, no `use()` needed). It is **polymorphic** — its behavior depends on the types of its arguments.
|
||||
|
||||
## Creation
|
||||
## From a Record
|
||||
|
||||
### object(obj)
|
||||
|
||||
@@ -29,6 +34,8 @@ Select specific keys.
|
||||
object({a: 1, b: 2, c: 3}, ["a", "c"]) // {a: 1, c: 3}
|
||||
```
|
||||
|
||||
## From an Array of Keys
|
||||
|
||||
### object(keys)
|
||||
|
||||
Create object from keys (values are `true`).
|
||||
@@ -60,9 +67,9 @@ object(["a", "b", "c"], (k, i) => i) // {a: 0, b: 1, c: 2}
|
||||
Create a new object with the given prototype.
|
||||
|
||||
```javascript
|
||||
var animal = {speak: function() { log.console("...") }}
|
||||
var animal = {speak: function() { print("...") }}
|
||||
var dog = meme(animal)
|
||||
dog.speak = function() { log.console("woof") }
|
||||
dog.speak = function() { print("woof") }
|
||||
```
|
||||
|
||||
### proto(obj)
|
||||
@@ -104,9 +111,4 @@ var obj = {a: 1, b: 2, c: 3}
|
||||
|
||||
// Get all keys
|
||||
var keys = array(obj) // ["a", "b", "c"]
|
||||
|
||||
// Iterate
|
||||
for (var key in obj) {
|
||||
log.console(key, obj[key])
|
||||
}
|
||||
```
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
# random
|
||||
---
|
||||
title: "random"
|
||||
description: "Random number generation"
|
||||
weight: 90
|
||||
type: "docs"
|
||||
---
|
||||
|
||||
Random number generation.
|
||||
|
||||
@@ -43,7 +48,7 @@ var random = use('random')
|
||||
var coin_flip = random.random() < 0.5
|
||||
|
||||
// Random element from array
|
||||
function pick(arr) {
|
||||
var pick = function(arr) {
|
||||
return arr[random.random_whole(length(arr))]
|
||||
}
|
||||
|
||||
@@ -51,11 +56,14 @@ var colors = ["red", "green", "blue"]
|
||||
var color = pick(colors)
|
||||
|
||||
// Shuffle array
|
||||
function shuffle(arr) {
|
||||
var shuffle = function(arr) {
|
||||
var result = array(arr) // copy
|
||||
for (var i = length(result) - 1; i > 0; i--) {
|
||||
var j = random.random_whole(i + 1)
|
||||
var temp = result[i]
|
||||
var i = length(result) - 1
|
||||
var j = 0
|
||||
var temp = null
|
||||
for (i = length(result) - 1; i > 0; i--) {
|
||||
j = random.random_whole(i + 1)
|
||||
temp = result[i]
|
||||
result[i] = result[j]
|
||||
result[j] = temp
|
||||
}
|
||||
@@ -63,8 +71,8 @@ function shuffle(arr) {
|
||||
}
|
||||
|
||||
// Random in range
|
||||
function random_range(min, max) {
|
||||
return min + random.random() * (max - min)
|
||||
var random_range = function(lo, hi) {
|
||||
return lo + random.random() * (hi - lo)
|
||||
}
|
||||
|
||||
var x = random_range(-10, 10) // -10 to 10
|
||||
|
||||
@@ -1,19 +1,28 @@
|
||||
# text
|
||||
---
|
||||
title: "text"
|
||||
description: "String conversion and manipulation"
|
||||
weight: 10
|
||||
type: "docs"
|
||||
---
|
||||
|
||||
The `text` function and its methods handle string conversion and manipulation.
|
||||
The `text` function is an intrinsic (always available, no `use()` needed). It is **polymorphic** — its behavior depends on the type of the first argument.
|
||||
|
||||
## Conversion
|
||||
To split text into characters, use `array(text)` — see [array](/docs/library/array/).
|
||||
|
||||
## From an Array
|
||||
|
||||
### text(array, separator)
|
||||
|
||||
Convert an array to text, joining elements with a separator (default: space).
|
||||
Join array elements into text with a separator (default: empty string).
|
||||
|
||||
```javascript
|
||||
text([1, 2, 3]) // "1 2 3"
|
||||
text([1, 2, 3], ", ") // "1, 2, 3"
|
||||
text(["a", "b"], "-") // "a-b"
|
||||
text(["h", "e", "l", "l", "o"]) // "hello"
|
||||
text([1, 2, 3], ", ") // "1, 2, 3"
|
||||
text(["a", "b"], "-") // "a-b"
|
||||
```
|
||||
|
||||
## From a Number
|
||||
|
||||
### text(number, radix)
|
||||
|
||||
Convert a number to text. Radix is 2-36 (default: 10).
|
||||
@@ -24,13 +33,16 @@ text(255, 16) // "ff"
|
||||
text(255, 2) // "11111111"
|
||||
```
|
||||
|
||||
## From Text
|
||||
|
||||
### text(text, from, to)
|
||||
|
||||
Extract a substring from index `from` to `to`.
|
||||
Extract a substring from index `from` to `to`. Negative indices count from end.
|
||||
|
||||
```javascript
|
||||
text("hello world", 0, 5) // "hello"
|
||||
text("hello world", 6) // "world"
|
||||
text("hello", -3) // "llo"
|
||||
```
|
||||
|
||||
## Methods
|
||||
@@ -70,18 +82,18 @@ text.search("hello world", "xyz") // null
|
||||
text.search("hello hello", "hello", 1) // 6
|
||||
```
|
||||
|
||||
### text.replace(text, target, replacement, limit)
|
||||
### text.replace(text, target, replacement, cap)
|
||||
|
||||
Replace occurrences of `target` with `replacement`.
|
||||
Replace occurrences of `target` with `replacement`. If `cap` is not specified, replaces all occurrences.
|
||||
|
||||
```javascript
|
||||
text.replace("hello", "l", "L") // "heLLo"
|
||||
text.replace("hello", "l", "L", 1) // "heLlo"
|
||||
text.replace("hello", "l", "L") // "heLLo" (replaces all)
|
||||
text.replace("hello", "l", "L", 1) // "heLlo" (replaces first only)
|
||||
|
||||
// With function
|
||||
text.replace("hello", "l", function(match, pos) {
|
||||
return pos == 2 ? "L" : match
|
||||
}) // "heLlo"
|
||||
}) // "heLLo" (replaces all by default)
|
||||
```
|
||||
|
||||
### text.format(text, collection, transformer)
|
||||
@@ -101,7 +113,7 @@ text.format("{0} + {1} = {2}", [1, 2, 3])
|
||||
Unicode normalize the text (NFC form).
|
||||
|
||||
```javascript
|
||||
text.normalize("café") // normalized form
|
||||
text.normalize("cafe\u0301") // normalized form
|
||||
```
|
||||
|
||||
### text.codepoint(text)
|
||||
@@ -109,8 +121,7 @@ text.normalize("café") // normalized form
|
||||
Get the Unicode codepoint of the first character.
|
||||
|
||||
```javascript
|
||||
text.codepoint("A") // 65
|
||||
text.codepoint("😀") // 128512
|
||||
text.codepoint("A") // 65
|
||||
```
|
||||
|
||||
### text.extract(text, pattern, from, to)
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
# time
|
||||
---
|
||||
title: "time"
|
||||
description: "Time constants and conversion functions"
|
||||
weight: 60
|
||||
type: "docs"
|
||||
---
|
||||
|
||||
The time module provides time constants and conversion functions.
|
||||
|
||||
@@ -96,7 +101,7 @@ var last_week = now - time.week
|
||||
var later = now + (2 * time.hour)
|
||||
|
||||
// Format future time
|
||||
log.console(time.text(tomorrow))
|
||||
print(time.text(tomorrow))
|
||||
```
|
||||
|
||||
## Example
|
||||
@@ -108,9 +113,9 @@ var time = use('time')
|
||||
var start = time.number()
|
||||
// ... do work ...
|
||||
var elapsed = time.number() - start
|
||||
log.console(`Took ${elapsed} seconds`)
|
||||
print(`Took ${elapsed} seconds`)
|
||||
|
||||
// Schedule for tomorrow
|
||||
var tomorrow = time.number() + time.day
|
||||
log.console(`Tomorrow: ${time.text(tomorrow, "yyyy-MM-dd")}`)
|
||||
print(`Tomorrow: ${time.text(tomorrow, "yyyy-MM-dd")}`)
|
||||
```
|
||||
|
||||
156
docs/nota.md
Normal file
156
docs/nota.md
Normal file
@@ -0,0 +1,156 @@
|
||||
---
|
||||
title: "Nota Format"
|
||||
description: "Network Object Transfer Arrangement"
|
||||
weight: 85
|
||||
type: "docs"
|
||||
---
|
||||
|
||||
Nota is a binary message format developed for use in the Procession Protocol. It provides a compact, JSON-like encoding that supports blobs, text, arrays, records, numbers, and symbols.
|
||||
|
||||
Nota stands for Network Object Transfer Arrangement.
|
||||
|
||||
## Design Philosophy
|
||||
|
||||
JSON had three design rules: minimal, textual, and subset of JavaScript. The textual and JavaScript rules are no longer necessary. Nota maintains JSON's philosophy of being at the intersection of most programming languages and most data types, but departs by using counts instead of brackets and binary encoding instead of text.
|
||||
|
||||
Nota uses Kim continuation bytes for counts and character encoding. See [Kim Encoding](#kim) for details.
|
||||
|
||||
## Type Summary
|
||||
|
||||
| Bits | Type |
|
||||
|------|------|
|
||||
| `000` | Blob |
|
||||
| `001` | Text |
|
||||
| `010` | Array |
|
||||
| `011` | Record |
|
||||
| `100` | Floating Point (positive exponent) |
|
||||
| `101` | Floating Point (negative exponent) |
|
||||
| `110` | Integer (zero exponent) |
|
||||
| `111` | Symbol |
|
||||
|
||||
## Preambles
|
||||
|
||||
Every Nota value starts with a preamble byte that is a Kim value with the three most significant bits used for type information.
|
||||
|
||||
Most types provide 3 or 4 data bits in the preamble. If the Kim encoding of the data fits in those bits, it is incorporated directly and the continue bit is off. Otherwise the continue bit is on and the continuation follows.
|
||||
|
||||
## Blob
|
||||
|
||||
```
|
||||
C 0 0 0 D D D D
|
||||
```
|
||||
|
||||
- **C** — continue the number of bits
|
||||
- **DDDD** — the number of bits
|
||||
|
||||
A blob is a string of bits. The data produces the number of bits. The number of bytes that follow: `floor((number_of_bits + 7) / 8)`. The final byte is padded with 0 if necessary.
|
||||
|
||||
Example: A blob containing 25 bits `1111000011100011001000001`:
|
||||
|
||||
```
|
||||
80 19 F0 E3 20 80
|
||||
```
|
||||
|
||||
## Text
|
||||
|
||||
```
|
||||
C 0 0 1 D D D D
|
||||
```
|
||||
|
||||
- **C** — continue the number of characters
|
||||
- **DDDD** — the number of characters
|
||||
|
||||
The data produces the number of characters. Kim-encoded characters follow. ASCII characters are 1 byte, first quarter BMP characters are 2 bytes, all other Unicode characters are 3 bytes. Unlike JSON, there is never a need for escapement.
|
||||
|
||||
Examples:
|
||||
|
||||
```
|
||||
"" → 10
|
||||
"cat" → 13 63 61 74
|
||||
```
|
||||
|
||||
## Array
|
||||
|
||||
```
|
||||
C 0 1 0 D D D D
|
||||
```
|
||||
|
||||
- **C** — continue the number of elements
|
||||
- **DDDD** — the number of elements
|
||||
|
||||
An array is an ordered sequence of values. Following the preamble are the elements, each beginning with its own preamble. Nesting is encouraged.
|
||||
|
||||
## Record
|
||||
|
||||
```
|
||||
C 0 1 1 D D D D
|
||||
```
|
||||
|
||||
- **C** — continue the number of pairs
|
||||
- **DDDD** — the number of pairs
|
||||
|
||||
A record is an unordered collection of key/value pairs. Keys must be text and must be unique within the record. Values can be any Nota type.
|
||||
|
||||
## Floating Point
|
||||
|
||||
```
|
||||
C 1 0 E S D D D
|
||||
```
|
||||
|
||||
- **C** — continue the exponent
|
||||
- **E** — sign of the exponent
|
||||
- **S** — sign of the coefficient
|
||||
- **DDD** — three bits of the exponent
|
||||
|
||||
Nota floating point represents numbers as `coefficient * 10^exponent`. The coefficient must be an integer. The preamble may contain the first three bits of the exponent, followed by the continuation of the exponent (if any), followed by the coefficient.
|
||||
|
||||
Use the integer type when the exponent is zero.
|
||||
|
||||
Examples:
|
||||
|
||||
```
|
||||
-1.01 → 5A 65
|
||||
98.6 → 51 87 5A
|
||||
-0.5772156649 → D8 0A 95 C0 B0 BD 69
|
||||
-10000000000000 → C8 0D 01
|
||||
```
|
||||
|
||||
## Integer
|
||||
|
||||
```
|
||||
C 1 1 0 S D D D
|
||||
```
|
||||
|
||||
- **C** — continue the integer
|
||||
- **S** — sign
|
||||
- **DDD** — three bits of the integer
|
||||
|
||||
Integers in the range -7 to 7 fit in a single byte. Integers in the range -1023 to 1023 fit in two bytes. Integers in the range -131071 to 131071 fit in three bytes.
|
||||
|
||||
Examples:
|
||||
|
||||
```
|
||||
0 → 60
|
||||
2023 → E0 8F 67
|
||||
-1 → 69
|
||||
```
|
||||
|
||||
## Symbol
|
||||
|
||||
```
|
||||
0 1 1 1 D D D D
|
||||
```
|
||||
|
||||
- **DDDD** — the symbol
|
||||
|
||||
There are currently five symbols:
|
||||
|
||||
```
|
||||
null → 70
|
||||
false → 72
|
||||
true → 73
|
||||
private → 78
|
||||
system → 79
|
||||
```
|
||||
|
||||
The private prefix must be followed by a record containing a private process address. The system prefix must be followed by a record containing a system message. All other symbols are reserved.
|
||||
@@ -1,14 +1,19 @@
|
||||
# Packages
|
||||
---
|
||||
title: "Packages"
|
||||
description: "Code organization and sharing in ƿit"
|
||||
weight: 30
|
||||
type: "docs"
|
||||
---
|
||||
|
||||
Packages are the fundamental unit of code organization and sharing in Cell.
|
||||
Packages are the fundamental unit of code organization and sharing in ƿit.
|
||||
|
||||
## Package Structure
|
||||
|
||||
A package is a directory containing a `cell.toml` manifest:
|
||||
A package is a directory containing a `pit.toml` manifest:
|
||||
|
||||
```
|
||||
mypackage/
|
||||
├── cell.toml # package manifest
|
||||
├── pit.toml # package manifest
|
||||
├── main.ce # entry point (optional)
|
||||
├── utils.cm # module
|
||||
├── helper/
|
||||
@@ -17,7 +22,7 @@ mypackage/
|
||||
└── _internal.cm # private module (underscore prefix)
|
||||
```
|
||||
|
||||
## cell.toml
|
||||
## pit.toml
|
||||
|
||||
The package manifest declares metadata and dependencies:
|
||||
|
||||
@@ -38,11 +43,11 @@ mylib = "/Users/john/work/mylib"
|
||||
|
||||
## Module Resolution
|
||||
|
||||
When importing with `use()`, Cell searches in order:
|
||||
When importing with `use()`, ƿit searches in order:
|
||||
|
||||
1. **Local package** — relative to package root
|
||||
2. **Dependencies** — via aliases in `cell.toml`
|
||||
3. **Core** — built-in Cell modules
|
||||
2. **Dependencies** — via aliases in `pit.toml`
|
||||
3. **Core** — built-in ƿit modules
|
||||
|
||||
```javascript
|
||||
// In package 'myapp' with dependency: renderer = "gitea.pockle.world/john/renderer"
|
||||
@@ -85,10 +90,10 @@ Local packages are symlinked into the shop, making development seamless.
|
||||
|
||||
## The Shop
|
||||
|
||||
Cell stores all packages in the **shop** at `~/.cell/`:
|
||||
ƿit stores all packages in the **shop** at `~/.pit/`:
|
||||
|
||||
```
|
||||
~/.cell/
|
||||
~/.pit/
|
||||
├── packages/
|
||||
│ ├── core -> gitea.pockle.world/john/cell
|
||||
│ ├── gitea.pockle.world/
|
||||
@@ -134,20 +139,20 @@ target = "/Users/john/work/prosperon"
|
||||
|
||||
```bash
|
||||
# Install from remote
|
||||
cell install gitea.pockle.world/john/prosperon
|
||||
pit install gitea.pockle.world/john/prosperon
|
||||
|
||||
# Install from local path
|
||||
cell install /Users/john/work/mylib
|
||||
pit install /Users/john/work/mylib
|
||||
```
|
||||
|
||||
## Updating Packages
|
||||
|
||||
```bash
|
||||
# Update all
|
||||
cell update
|
||||
pit update
|
||||
|
||||
# Update specific package
|
||||
cell update gitea.pockle.world/john/prosperon
|
||||
pit update gitea.pockle.world/john/prosperon
|
||||
```
|
||||
|
||||
## Development Workflow
|
||||
@@ -156,12 +161,12 @@ For active development, link packages locally:
|
||||
|
||||
```bash
|
||||
# Link a package for development
|
||||
cell link add gitea.pockle.world/john/prosperon /Users/john/work/prosperon
|
||||
pit link add gitea.pockle.world/john/prosperon /Users/john/work/prosperon
|
||||
|
||||
# Changes to /Users/john/work/prosperon are immediately visible
|
||||
|
||||
# Remove link when done
|
||||
cell link delete gitea.pockle.world/john/prosperon
|
||||
pit link delete gitea.pockle.world/john/prosperon
|
||||
```
|
||||
|
||||
## C Extensions
|
||||
@@ -170,14 +175,14 @@ C files in a package are compiled into a dynamic library:
|
||||
|
||||
```
|
||||
mypackage/
|
||||
├── cell.toml
|
||||
├── pit.toml
|
||||
├── render.c # compiled to mypackage.dylib
|
||||
└── render.cm # optional Cell wrapper
|
||||
└── render.cm # optional ƿit wrapper
|
||||
```
|
||||
|
||||
The library is named after the package and placed in `~/.cell/lib/`.
|
||||
The library is named after the package and placed in `~/.pit/lib/`.
|
||||
|
||||
See [Writing C Modules](c-modules.md) for details.
|
||||
See [Writing C Modules](/docs/c-modules/) for details.
|
||||
|
||||
## Platform-Specific Files
|
||||
|
||||
@@ -190,4 +195,4 @@ mypackage/
|
||||
└── audio_emscripten.c # Web-specific
|
||||
```
|
||||
|
||||
Cell selects the appropriate file based on the build target.
|
||||
ƿit selects the appropriate file based on the build target.
|
||||
|
||||
176
docs/requestors.md
Normal file
176
docs/requestors.md
Normal file
@@ -0,0 +1,176 @@
|
||||
---
|
||||
title: "Requestors"
|
||||
description: "Asynchronous work with requestors"
|
||||
weight: 25
|
||||
type: "docs"
|
||||
---
|
||||
|
||||
Requestors are functions that encapsulate asynchronous work. They provide a structured way to compose callbacks, manage cancellation, and coordinate concurrent operations between actors.
|
||||
|
||||
## What is a Requestor
|
||||
|
||||
A requestor is a function with this signature:
|
||||
|
||||
```javascript
|
||||
var my_requestor = function(callback, value) {
|
||||
// Do async work, then call callback with result
|
||||
// Return a cancel function
|
||||
}
|
||||
```
|
||||
|
||||
- **callback** — called when the work completes: `callback(value, reason)`
|
||||
- On success: `callback(result)` or `callback(result, null)`
|
||||
- On failure: `callback(null, reason)` where reason explains the failure
|
||||
- **value** — input passed from the previous step (or the initial caller)
|
||||
- **return** — a cancel function, or null if cancellation is not supported
|
||||
|
||||
The cancel function, when called, should abort the in-progress work.
|
||||
|
||||
## Writing a Requestor
|
||||
|
||||
```javascript
|
||||
var fetch_data = function(callback, url) {
|
||||
$contact(function(connection) {
|
||||
$send(connection, {get: url}, function(response) {
|
||||
callback(response)
|
||||
})
|
||||
}, {host: url, port: 80})
|
||||
return function() {
|
||||
// clean up if needed
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
A requestor that always succeeds immediately:
|
||||
|
||||
```javascript
|
||||
var constant = function(callback, value) {
|
||||
callback(42)
|
||||
}
|
||||
```
|
||||
|
||||
A requestor that always fails:
|
||||
|
||||
```javascript
|
||||
var broken = function(callback, value) {
|
||||
callback(null, "something went wrong")
|
||||
}
|
||||
```
|
||||
|
||||
## Composing Requestors
|
||||
|
||||
ƿit provides four built-in functions for composing requestors into pipelines.
|
||||
|
||||
### sequence(requestor_array)
|
||||
|
||||
Run requestors one after another. Each result becomes the input to the next. The final result is passed to the callback.
|
||||
|
||||
```javascript
|
||||
var pipeline = sequence([
|
||||
fetch_user,
|
||||
validate_permissions,
|
||||
load_profile
|
||||
])
|
||||
|
||||
pipeline(function(profile, reason) {
|
||||
if (reason) {
|
||||
print(reason)
|
||||
} else {
|
||||
print(profile.name)
|
||||
}
|
||||
}, user_id)
|
||||
```
|
||||
|
||||
If any step fails, the remaining steps are skipped and the failure propagates.
|
||||
|
||||
### parallel(requestor_array, throttle, need)
|
||||
|
||||
Start all requestors concurrently. Results are collected into an array matching the input order.
|
||||
|
||||
```javascript
|
||||
var both = parallel([
|
||||
fetch_profile,
|
||||
fetch_settings
|
||||
])
|
||||
|
||||
both(function(results, reason) {
|
||||
var profile = results[0]
|
||||
var settings = results[1]
|
||||
}, user_id)
|
||||
```
|
||||
|
||||
- **throttle** — limit how many requestors run at once (null for no limit)
|
||||
- **need** — minimum number of successes required (default: all)
|
||||
|
||||
### race(requestor_array, throttle, need)
|
||||
|
||||
Like `parallel`, but returns as soon as the needed number of results arrive. Unfinished requestors are cancelled.
|
||||
|
||||
```javascript
|
||||
var fastest = race([
|
||||
fetch_from_cache,
|
||||
fetch_from_network,
|
||||
fetch_from_backup
|
||||
])
|
||||
|
||||
fastest(function(results) {
|
||||
// results[0] is whichever responded first
|
||||
}, request)
|
||||
```
|
||||
|
||||
Default need is 1. Useful for redundant operations where only one result matters.
|
||||
|
||||
### fallback(requestor_array)
|
||||
|
||||
Try each requestor in order. If one fails, try the next. Return the first success.
|
||||
|
||||
```javascript
|
||||
var resilient = fallback([
|
||||
fetch_from_primary,
|
||||
fetch_from_secondary,
|
||||
use_cached_value
|
||||
])
|
||||
|
||||
resilient(function(data, reason) {
|
||||
if (reason) {
|
||||
print("all sources failed")
|
||||
}
|
||||
}, key)
|
||||
```
|
||||
|
||||
## Timeouts
|
||||
|
||||
Wrap any requestor with `$time_limit` to add a timeout:
|
||||
|
||||
```javascript
|
||||
var timed = $time_limit(fetch_data, 5) // 5 second timeout
|
||||
|
||||
timed(function(result, reason) {
|
||||
// reason will explain timeout if it fires
|
||||
}, url)
|
||||
```
|
||||
|
||||
If the requestor does not complete within the time limit, it is cancelled and the callback receives a failure.
|
||||
|
||||
## Requestors and Actors
|
||||
|
||||
Requestors are particularly useful with actor messaging. Since `$send` is callback-based, it fits naturally:
|
||||
|
||||
```javascript
|
||||
var ask_worker = function(callback, task) {
|
||||
$send(worker, task, function(reply) {
|
||||
callback(reply)
|
||||
})
|
||||
}
|
||||
|
||||
var pipeline = sequence([
|
||||
ask_worker,
|
||||
process_result,
|
||||
store_result
|
||||
])
|
||||
|
||||
pipeline(function(stored) {
|
||||
print("done")
|
||||
$stop()
|
||||
}, {type: "compute", data: [1, 2, 3]})
|
||||
```
|
||||
77
docs/spec/dec64.md
Normal file
77
docs/spec/dec64.md
Normal file
@@ -0,0 +1,77 @@
|
||||
---
|
||||
title: "DEC64 Numbers"
|
||||
description: "Decimal floating point representation"
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
ƿit uses DEC64 as its number format. DEC64 represents numbers as `coefficient * 10^exponent` in a 64-bit word. This eliminates the rounding errors that plague IEEE 754 binary floating point — `0.1 + 0.2` is exactly `0.3`.
|
||||
|
||||
DEC64 was designed by Douglas Crockford as a general-purpose number type suitable for both business and scientific computation.
|
||||
|
||||
## Format
|
||||
|
||||
A DEC64 number is a 64-bit value:
|
||||
|
||||
```
|
||||
[coefficient: 56 bits][exponent: 8 bits]
|
||||
```
|
||||
|
||||
- **Coefficient** — a 56-bit signed integer (two's complement)
|
||||
- **Exponent** — an 8-bit signed integer (range: -127 to 127)
|
||||
|
||||
The value of a DEC64 number is: `coefficient * 10^exponent`
|
||||
|
||||
### Examples
|
||||
|
||||
| Value | Coefficient | Exponent | Hex |
|
||||
|-------|------------|----------|-----|
|
||||
| `0` | 0 | 0 | `0000000000000000` |
|
||||
| `1` | 1 | 0 | `0000000000000100` |
|
||||
| `3.14159` | 314159 | -5 | `000000004CB2FFFB` |
|
||||
| `-1` | -1 | 0 | `FFFFFFFFFFFFFF00` |
|
||||
| `1000000` | 1 | 6 | `0000000000000106` |
|
||||
|
||||
## Special Values
|
||||
|
||||
### Null
|
||||
|
||||
The exponent `0x80` (-128) indicates null. This is the only special value — there is no infinity, no NaN, no negative zero. Operations that would produce undefined results (such as division by zero) return null.
|
||||
|
||||
```
|
||||
coefficient: any, exponent: 0x80 → null
|
||||
```
|
||||
|
||||
## Arithmetic Properties
|
||||
|
||||
- **Exact decimals**: All decimal fractions with up to 17 significant digits are represented exactly
|
||||
- **No rounding**: `0.1 + 0.2 == 0.3` is true
|
||||
- **Integer range**: Exact integers up to 2^55 (about 3.6 * 10^16)
|
||||
- **Normalized on demand**: The runtime normalizes coefficients to remove trailing zeros when needed for comparison
|
||||
|
||||
## Comparison with IEEE 754
|
||||
|
||||
| Property | DEC64 | IEEE 754 double |
|
||||
|----------|-------|----------------|
|
||||
| Decimal fractions | Exact | Approximate |
|
||||
| Significant digits | ~17 | ~15-16 |
|
||||
| Special values | null only | NaN, ±Infinity, -0 |
|
||||
| Rounding errors | None (decimal) | Common |
|
||||
| Financial arithmetic | Correct | Requires libraries |
|
||||
| Scientific range | ±10^127 | ±10^308 |
|
||||
|
||||
DEC64 trades a smaller exponent range for exact decimal arithmetic. Most applications never need exponents beyond ±127.
|
||||
|
||||
## In ƿit
|
||||
|
||||
All numbers in ƿit are DEC64. There is no separate integer type at the language level — the distinction is internal. The `is_integer` function checks whether a number has no fractional part.
|
||||
|
||||
```javascript
|
||||
var x = 42 // coefficient: 42, exponent: 0
|
||||
var y = 3.14 // coefficient: 314, exponent: -2
|
||||
var z = 1000000 // coefficient: 1, exponent: 6 (normalized)
|
||||
|
||||
is_integer(x) // true
|
||||
is_integer(y) // false
|
||||
1 / 0 // null
|
||||
```
|
||||
82
docs/spec/gc.md
Normal file
82
docs/spec/gc.md
Normal file
@@ -0,0 +1,82 @@
|
||||
---
|
||||
title: "Garbage Collection"
|
||||
description: "Cheney copying collector"
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
ƿit uses a Cheney copying collector for automatic memory management. Each actor has its own independent heap — actors never share mutable memory, so garbage collection is per-actor with no global pauses.
|
||||
|
||||
## Algorithm
|
||||
|
||||
The Cheney algorithm is a two-space copying collector:
|
||||
|
||||
1. **Allocate new space** — a fresh memory block for the new heap
|
||||
2. **Copy roots** — copy all live root objects from old space to new space
|
||||
3. **Scan** — walk the new space, updating all internal references
|
||||
4. **Free old space** — the entire old heap is freed at once
|
||||
|
||||
### Copying and Forwarding
|
||||
|
||||
When an object is copied from old space to new space:
|
||||
|
||||
1. The object's data is copied to the next free position in new space
|
||||
2. The old object's header is overwritten with a **forwarding pointer** (`OBJ_FORWARD`) containing the new address
|
||||
3. Future references to the old address find the forwarding pointer and follow it to the new location
|
||||
|
||||
```
|
||||
Old space: New space:
|
||||
┌──────────────┐ ┌──────────────┐
|
||||
│ OBJ_FORWARD ─┼────────> │ copied object│
|
||||
│ (new addr) │ │ │
|
||||
└──────────────┘ └──────────────┘
|
||||
```
|
||||
|
||||
### Scan Phase
|
||||
|
||||
After roots are copied, the collector scans new space linearly. For each object, it examines every JSValue field:
|
||||
|
||||
- If the field points to old space, copy the referenced object (or follow its forwarding pointer if already copied)
|
||||
- If the field points to stone memory, skip it (stone objects are permanent)
|
||||
- If the field is an immediate value (integer, boolean, null, immediate string), skip it
|
||||
|
||||
The scan continues until the scan pointer catches up with the allocation pointer — at that point, all live objects have been found and copied.
|
||||
|
||||
## Roots
|
||||
|
||||
The collector traces from these root sources:
|
||||
|
||||
- **Global object** — all global variables
|
||||
- **Class prototypes** — built-in type prototypes
|
||||
- **Exception** — the current exception value
|
||||
- **Value stack** — all values on the operand stack
|
||||
- **Frame stack** — all stack frames (register VM and mcode)
|
||||
- **GC reference stack** — manually registered roots (via `JS_PUSH_VALUE` / `JS_POP_VALUE`)
|
||||
- **Parser constant pool** — during compilation, constants being built
|
||||
|
||||
## Per-Actor Heaps
|
||||
|
||||
Each actor maintains its own heap with independent collection:
|
||||
|
||||
- No stop-the-world pauses across actors
|
||||
- No synchronization between collectors
|
||||
- Each actor's GC runs at the end of a turn (between message deliveries)
|
||||
- Heap sizes adapt independently based on each actor's allocation patterns
|
||||
|
||||
## Heap Growth
|
||||
|
||||
The collector uses a buddy allocator for heap blocks. After each collection, if less than 20% of the heap was recovered, the next block size is doubled. The new space size is: `max(live_estimate + alloc_size, next_block_size)`.
|
||||
|
||||
All allocations within a heap block use bump allocation (advance a pointer), which is extremely fast.
|
||||
|
||||
## Alignment
|
||||
|
||||
All objects are aligned to 8-byte boundaries. Object sizes are rounded up to ensure this alignment, which guarantees that the low 3 bits of any heap pointer are always zero — available for JSValue tag bits.
|
||||
|
||||
## Interaction with Stone Memory
|
||||
|
||||
Stone memory objects (S bit set) are never copied by the collector. When the scanner encounters a pointer to stone memory, it leaves it unchanged. This means:
|
||||
|
||||
- Stone objects are effectively permanent GC roots
|
||||
- No overhead for tracing through immutable object graphs
|
||||
- Module return values and interned strings impose zero GC cost
|
||||
156
docs/spec/mach.md
Normal file
156
docs/spec/mach.md
Normal file
@@ -0,0 +1,156 @@
|
||||
---
|
||||
title: "Register VM"
|
||||
description: "Register-based virtual machine (Mach)"
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
The Mach VM is a register-based virtual machine using 32-bit instructions. It is modeled after Lua's register VM — operands are register indices rather than stack positions, reducing instruction count and improving performance.
|
||||
|
||||
## Instruction Formats
|
||||
|
||||
All instructions are 32 bits wide. Four encoding formats are used:
|
||||
|
||||
### iABC — Three-Register
|
||||
|
||||
```
|
||||
[op: 8][A: 8][B: 8][C: 8]
|
||||
```
|
||||
|
||||
Used for operations on three registers: `R(A) = R(B) op R(C)`.
|
||||
|
||||
### iABx — Register + Constant
|
||||
|
||||
```
|
||||
[op: 8][A: 8][Bx: 16]
|
||||
```
|
||||
|
||||
Used for loading constants: `R(A) = K(Bx)`.
|
||||
|
||||
### iAsBx — Register + Signed Offset
|
||||
|
||||
```
|
||||
[op: 8][A: 8][sBx: 16]
|
||||
```
|
||||
|
||||
Used for conditional jumps: if `R(A)` then jump by `sBx`.
|
||||
|
||||
### isJ — Signed Jump
|
||||
|
||||
```
|
||||
[op: 8][sJ: 24]
|
||||
```
|
||||
|
||||
Used for unconditional jumps with a 24-bit signed offset.
|
||||
|
||||
## Registers
|
||||
|
||||
Each function frame has a fixed number of register slots, determined at compile time. Registers hold:
|
||||
|
||||
- **R(0)** — `this` binding
|
||||
- **R(1)..R(arity)** — function arguments
|
||||
- **R(arity+1)..** — local variables and temporaries
|
||||
|
||||
## Instruction Set
|
||||
|
||||
### Loading
|
||||
|
||||
| Opcode | Format | Description |
|
||||
|--------|--------|-------------|
|
||||
| `LOADK` | iABx | `R(A) = K(Bx)` — load from constant pool |
|
||||
| `LOADI` | iAsBx | `R(A) = sBx` — load small integer |
|
||||
| `LOADNULL` | iA | `R(A) = null` |
|
||||
| `LOADTRUE` | iA | `R(A) = true` |
|
||||
| `LOADFALSE` | iA | `R(A) = false` |
|
||||
| `MOVE` | iABC | `R(A) = R(B)` — register copy |
|
||||
|
||||
### Arithmetic
|
||||
|
||||
| Opcode | Format | Description |
|
||||
|--------|--------|-------------|
|
||||
| `ADD` | iABC | `R(A) = R(B) + R(C)` |
|
||||
| `SUB` | iABC | `R(A) = R(B) - R(C)` |
|
||||
| `MUL` | iABC | `R(A) = R(B) * R(C)` |
|
||||
| `DIV` | iABC | `R(A) = R(B) / R(C)` |
|
||||
| `MOD` | iABC | `R(A) = R(B) % R(C)` |
|
||||
| `POW` | iABC | `R(A) = R(B) ^ R(C)` |
|
||||
| `NEG` | iABC | `R(A) = -R(B)` |
|
||||
| `INC` | iABC | `R(A) = R(B) + 1` |
|
||||
| `DEC` | iABC | `R(A) = R(B) - 1` |
|
||||
|
||||
### Comparison
|
||||
|
||||
| Opcode | Format | Description |
|
||||
|--------|--------|-------------|
|
||||
| `EQ` | iABC | `R(A) = R(B) == R(C)` |
|
||||
| `NEQ` | iABC | `R(A) = R(B) != R(C)` |
|
||||
| `LT` | iABC | `R(A) = R(B) < R(C)` |
|
||||
| `LE` | iABC | `R(A) = R(B) <= R(C)` |
|
||||
| `GT` | iABC | `R(A) = R(B) > R(C)` |
|
||||
| `GE` | iABC | `R(A) = R(B) >= R(C)` |
|
||||
|
||||
### Property Access
|
||||
|
||||
| Opcode | Format | Description |
|
||||
|--------|--------|-------------|
|
||||
| `GETFIELD` | iABC | `R(A) = R(B)[K(C)]` — named property |
|
||||
| `SETFIELD` | iABC | `R(A)[K(B)] = R(C)` — set named property |
|
||||
| `GETINDEX` | iABC | `R(A) = R(B)[R(C)]` — computed property |
|
||||
| `SETINDEX` | iABC | `R(A)[R(B)] = R(C)` — set computed property |
|
||||
|
||||
### Variable Resolution
|
||||
|
||||
| Opcode | Format | Description |
|
||||
|--------|--------|-------------|
|
||||
| `GETNAME` | iABx | Unresolved variable (compiler placeholder) |
|
||||
| `GETINTRINSIC` | iABx | Global intrinsic / built-in |
|
||||
| `GETENV` | iABx | Module environment variable |
|
||||
| `GETUP` | iABC | `R(A) = UpFrame(B).slots[C]` — closure upvalue |
|
||||
| `SETUP` | iABC | `UpFrame(A).slots[B] = R(C)` — set closure upvalue |
|
||||
|
||||
### Control Flow
|
||||
|
||||
| Opcode | Format | Description |
|
||||
|--------|--------|-------------|
|
||||
| `JMP` | isJ | Unconditional jump |
|
||||
| `JMPTRUE` | iAsBx | Jump if `R(A)` is true |
|
||||
| `JMPFALSE` | iAsBx | Jump if `R(A)` is false |
|
||||
| `JMPNULL` | iAsBx | Jump if `R(A)` is null |
|
||||
|
||||
### Function Calls
|
||||
|
||||
| Opcode | Format | Description |
|
||||
|--------|--------|-------------|
|
||||
| `CALL` | iABC | Call `R(A)` with `B` args starting at `R(A+1)`, `C`=keep result |
|
||||
| `RETURN` | iA | Return `R(A)` |
|
||||
| `RETNIL` | — | Return null |
|
||||
| `CLOSURE` | iABx | Create closure from function pool entry `Bx` |
|
||||
|
||||
### Object / Array
|
||||
|
||||
| Opcode | Format | Description |
|
||||
|--------|--------|-------------|
|
||||
| `NEWOBJECT` | iA | `R(A) = {}` |
|
||||
| `NEWARRAY` | iABC | `R(A) = array(B)` |
|
||||
| `PUSH` | iABC | Push `R(B)` to array `R(A)` |
|
||||
|
||||
## JSCodeRegister
|
||||
|
||||
The compiled output for a function:
|
||||
|
||||
```c
|
||||
struct JSCodeRegister {
|
||||
uint16_t arity; // argument count
|
||||
uint16_t nr_slots; // total register count
|
||||
uint32_t cpool_count; // constant pool size
|
||||
JSValue *cpool; // constant pool
|
||||
uint32_t instr_count; // instruction count
|
||||
MachInstr32 *instructions; // 32-bit instruction array
|
||||
uint32_t func_count; // nested function count
|
||||
JSCodeRegister **functions; // nested function table
|
||||
JSValue name; // function name
|
||||
uint16_t disruption_pc; // exception handler offset
|
||||
};
|
||||
```
|
||||
|
||||
The constant pool holds all non-immediate values referenced by `LOADK` instructions: strings, large numbers, and other constants.
|
||||
90
docs/spec/mcode.md
Normal file
90
docs/spec/mcode.md
Normal file
@@ -0,0 +1,90 @@
|
||||
---
|
||||
title: "Mcode IR"
|
||||
description: "JSON-based intermediate representation"
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Mcode is a JSON-based intermediate representation that can be interpreted directly. It represents the same operations as the Mach register VM but uses string-based instruction dispatch rather than binary opcodes. Mcode is intended as an intermediate step toward native code compilation.
|
||||
|
||||
## Pipeline
|
||||
|
||||
```
|
||||
Source → Tokenize → Parse (AST) → Mcode (JSON) → Interpret
|
||||
→ Compile to Mach (planned)
|
||||
→ Compile to native (planned)
|
||||
```
|
||||
|
||||
Mcode is produced by the `JS_Mcode` compiler pass, which emits a cJSON tree. The mcode interpreter walks this tree directly, dispatching on instruction name strings.
|
||||
|
||||
## JSMCode Structure
|
||||
|
||||
```c
|
||||
struct JSMCode {
|
||||
uint16_t nr_args; // argument count
|
||||
uint16_t nr_slots; // register count
|
||||
cJSON **instrs; // pre-flattened instruction array
|
||||
uint32_t instr_count; // number of instructions
|
||||
|
||||
struct {
|
||||
const char *name; // label name
|
||||
uint32_t index; // instruction index
|
||||
} *labels;
|
||||
uint32_t label_count;
|
||||
|
||||
struct JSMCode **functions; // nested functions
|
||||
uint32_t func_count;
|
||||
|
||||
cJSON *json_root; // keeps JSON alive
|
||||
const char *name; // function name
|
||||
const char *filename; // source file
|
||||
uint16_t disruption_pc; // exception handler offset
|
||||
};
|
||||
```
|
||||
|
||||
## Instruction Format
|
||||
|
||||
Each instruction is a JSON array. The first element is the instruction name (string), followed by operands:
|
||||
|
||||
```json
|
||||
["LOADK", 0, 42]
|
||||
["ADD", 2, 0, 1]
|
||||
["JMPFALSE", 3, "else_label"]
|
||||
["CALL", 0, 2, 1]
|
||||
```
|
||||
|
||||
The instruction set mirrors the Mach VM opcodes — same operations, same register semantics, but with string dispatch instead of numeric opcodes.
|
||||
|
||||
## Labels
|
||||
|
||||
Control flow uses named labels instead of numeric offsets:
|
||||
|
||||
```json
|
||||
["LABEL", "loop_start"]
|
||||
["ADD", 1, 1, 2]
|
||||
["JMPFALSE", 3, "loop_end"]
|
||||
["JMP", "loop_start"]
|
||||
["LABEL", "loop_end"]
|
||||
```
|
||||
|
||||
Labels are collected into a name-to-index map during loading, enabling O(1) jump resolution.
|
||||
|
||||
## Differences from Mach
|
||||
|
||||
| Property | Mcode | Mach |
|
||||
|----------|-------|------|
|
||||
| Instructions | cJSON arrays | 32-bit binary |
|
||||
| Dispatch | String comparison | Switch on opcode byte |
|
||||
| Constants | Inline in JSON | Separate constant pool |
|
||||
| Jump targets | Named labels | Numeric offsets |
|
||||
| Memory | Heap (cJSON nodes) | Off-heap (malloc) |
|
||||
|
||||
## Purpose
|
||||
|
||||
Mcode serves as an inspectable, debuggable intermediate format:
|
||||
|
||||
- **Human-readable** — the JSON representation can be printed and examined
|
||||
- **Language-independent** — any tool that produces the correct JSON can target the ƿit runtime
|
||||
- **Compilation target** — the Mach compiler can consume mcode as input, and future native code generators can work from the same representation
|
||||
|
||||
The cost of string-based dispatch makes mcode slower than the binary Mach VM, so it is primarily useful during development and as a compilation intermediate rather than for production execution.
|
||||
141
docs/spec/objects.md
Normal file
141
docs/spec/objects.md
Normal file
@@ -0,0 +1,141 @@
|
||||
---
|
||||
title: "Object Types"
|
||||
description: "Heap object header format and types"
|
||||
---
|
||||
|
||||
## Object Header
|
||||
|
||||
Every heap-allocated object begins with a 64-bit header word (`objhdr_t`):
|
||||
|
||||
```
|
||||
[capacity: 56 bits][flags: 5 bits][type: 3 bits]
|
||||
```
|
||||
|
||||
### Type Field (bits 0-2)
|
||||
|
||||
| Value | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| 0 | `OBJ_ARRAY` | Dynamic array of JSValues |
|
||||
| 1 | `OBJ_BLOB` | Binary data (bits) |
|
||||
| 2 | `OBJ_TEXT` | Unicode text string |
|
||||
| 3 | `OBJ_RECORD` | Key-value object with prototype chain |
|
||||
| 4 | `OBJ_FUNCTION` | Function (C, register, or mcode) |
|
||||
| 5 | `OBJ_CODE` | Compiled code |
|
||||
| 6 | `OBJ_FRAME` | Stack frame for closures |
|
||||
| 7 | `OBJ_FORWARD` | Forwarding pointer (GC) |
|
||||
|
||||
### Flags (bits 3-7)
|
||||
|
||||
- **Bit 3 (S)** — Stone flag. If set, the object is immutable and excluded from GC.
|
||||
- **Bit 4 (P)** — Properties flag.
|
||||
- **Bit 5 (A)** — Array flag.
|
||||
- **Bit 7 (R)** — Reserved.
|
||||
|
||||
### Capacity (bits 8-63)
|
||||
|
||||
The interpretation of the 56-bit capacity field depends on the object type.
|
||||
|
||||
## Array
|
||||
|
||||
```c
|
||||
struct JSArray {
|
||||
objhdr_t header; // type=0, capacity=element slots
|
||||
word_t len; // current number of elements
|
||||
JSValue values[]; // inline flexible array
|
||||
};
|
||||
```
|
||||
|
||||
Capacity is the number of JSValue slots allocated. Length is the number currently in use. Arrays grow by reallocating with a larger capacity.
|
||||
|
||||
## Blob
|
||||
|
||||
```c
|
||||
struct JSBlob {
|
||||
objhdr_t header; // type=1, capacity=allocated bits
|
||||
word_t length; // length in bits
|
||||
uint8_t bits[]; // bit-packed data
|
||||
};
|
||||
```
|
||||
|
||||
Blobs are bit-addressable. The length field tracks the exact number of bits written. A blob starts as antestone (mutable) for writing, then becomes stone (immutable) for reading.
|
||||
|
||||
## Text
|
||||
|
||||
```c
|
||||
struct JSText {
|
||||
objhdr_t header; // type=2, capacity=character slots
|
||||
word_t length; // length in codepoints (or hash if stoned)
|
||||
word_t packed[]; // two UTF-32 chars per 64-bit word
|
||||
};
|
||||
```
|
||||
|
||||
Text is stored as UTF-32, with two 32-bit codepoints packed per 64-bit word. When a text object is stoned, the length field is repurposed to cache the hash value (computed via `fash64`), since stoned text is immutable and the hash never changes.
|
||||
|
||||
## Record
|
||||
|
||||
```c
|
||||
struct JSRecord {
|
||||
objhdr_t header; // type=3, capacity=hash table slots
|
||||
JSRecord *proto; // prototype chain pointer
|
||||
word_t len; // number of entries
|
||||
slot slots[]; // key-value pairs (hash table)
|
||||
};
|
||||
```
|
||||
|
||||
Records use a hash table with linear probing. Slot 0 is reserved for internal metadata (class ID and record ID). Empty slots use `JS_NULL` as the key; deleted slots use `JS_EXCEPTION` as a tombstone.
|
||||
|
||||
The prototype chain is a linked list of JSRecord pointers, traversed during property lookup.
|
||||
|
||||
## Function
|
||||
|
||||
```c
|
||||
struct JSFunction {
|
||||
objhdr_t header; // type=4
|
||||
JSValue name; // function name
|
||||
int16_t length; // arity (-1 for variadic)
|
||||
uint8_t kind; // C, register, or mcode
|
||||
union {
|
||||
struct { ... } cfunc; // C function pointer
|
||||
struct { ... } regvm; // register VM code
|
||||
struct { ... } mcode; // mcode IR
|
||||
} u;
|
||||
};
|
||||
```
|
||||
|
||||
The kind field selects which union variant is active. Functions can be implemented in C (native), register code (mach VM), or mcode (JSON interpreter).
|
||||
|
||||
## Frame
|
||||
|
||||
```c
|
||||
struct JSFrame {
|
||||
objhdr_t header; // type=6, capacity=slot count
|
||||
JSValue function; // owning function
|
||||
JSValue caller; // parent frame
|
||||
uint32_t return_pc; // return address
|
||||
JSValue slots[]; // [this][args][captured][locals][temps]
|
||||
};
|
||||
```
|
||||
|
||||
Frames capture the execution context for closures. The slots array contains the function's `this` binding, arguments, captured upvalues, local variables, and temporaries. Frames are linked via the caller field for upvalue resolution across closure depth.
|
||||
|
||||
## Forwarding Pointer
|
||||
|
||||
```
|
||||
[pointer: 61 bits][111]
|
||||
```
|
||||
|
||||
During garbage collection, when an object is copied to the new heap, the old header is replaced with a forwarding pointer to the new location. This is type 7 (`OBJ_FORWARD`) and stores the new address in bits 3-63. See [Garbage Collection](#gc) for details.
|
||||
|
||||
## Object Sizing
|
||||
|
||||
All objects are aligned to 8 bytes. The total size in bytes for each type:
|
||||
|
||||
| Type | Size |
|
||||
|------|------|
|
||||
| Array | `8 + 8 + capacity * 8` |
|
||||
| Blob | `8 + 8 + ceil(capacity / 8)` |
|
||||
| Text | `8 + 8 + ceil(capacity / 2) * 8` |
|
||||
| Record | `8 + 8 + 8 + (capacity + 1) * 16` |
|
||||
| Function | `sizeof(JSFunction)` (fixed) |
|
||||
| Code | `sizeof(JSFunctionBytecode)` (fixed) |
|
||||
| Frame | `8 + 8 + 8 + 4 + capacity * 8` |
|
||||
82
docs/spec/stone.md
Normal file
82
docs/spec/stone.md
Normal file
@@ -0,0 +1,82 @@
|
||||
---
|
||||
title: "Stone Memory"
|
||||
description: "Immutable arena allocation"
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Stone memory is a separate allocation arena for immutable values. Objects in stone memory are permanent — they are never moved, never freed, and never touched by the garbage collector.
|
||||
|
||||
The `stone()` function in ƿit petrifies a value, deeply freezing it and all its descendants. Stoned objects have the S bit set in their object header.
|
||||
|
||||
## The Stone Arena
|
||||
|
||||
Stone memory uses bump allocation from a contiguous arena:
|
||||
|
||||
```
|
||||
stone_base ──────── stone_free ──────── stone_end
|
||||
[allocated objects] [free space ]
|
||||
```
|
||||
|
||||
Allocation advances `stone_free` forward. When the arena is exhausted, overflow pages are allocated via the system allocator and linked together:
|
||||
|
||||
```c
|
||||
struct StonePage {
|
||||
struct StonePage *next;
|
||||
size_t size;
|
||||
uint8_t data[];
|
||||
};
|
||||
```
|
||||
|
||||
## The S Bit
|
||||
|
||||
Bit 3 of the object header is the stone flag. When set:
|
||||
|
||||
- The object is **immutable** — writes disrupt
|
||||
- The object is **excluded from GC** — the collector skips it entirely
|
||||
- For text objects, the length field caches the **hash** instead of the character count (since the text cannot change, the hash is computed once and reused)
|
||||
|
||||
## What Gets Stoned
|
||||
|
||||
When `stone(value)` is called:
|
||||
|
||||
1. If the value is already stone, return immediately
|
||||
2. Recursively walk all nested values (array elements, record fields, etc.)
|
||||
3. Copy each mutable object into the stone arena
|
||||
4. Set the S bit on each copied object
|
||||
5. Return the stoned value
|
||||
|
||||
The operation is deep — an entire object graph becomes permanently immutable.
|
||||
|
||||
## Text Interning
|
||||
|
||||
The stone arena maintains a hash table for text interning. When a text value is stoned, it is looked up in the intern table. If an identical string already exists in stone memory, the existing one is reused. This deduplicates strings and makes equality comparison O(1) for stoned text.
|
||||
|
||||
The hash is computed with `fash64` over the packed UTF-32 words.
|
||||
|
||||
## Usage Patterns
|
||||
|
||||
### Module Return Values
|
||||
|
||||
Every module's return value is automatically stoned:
|
||||
|
||||
```javascript
|
||||
// config.cm
|
||||
return {
|
||||
debug: true,
|
||||
timeout: 30
|
||||
}
|
||||
// The returned object is stone — shared safely between actors
|
||||
```
|
||||
|
||||
### Message Passing
|
||||
|
||||
Messages between actors are stoned before delivery, ensuring actors never share mutable state.
|
||||
|
||||
### Constants
|
||||
|
||||
Literal objects and arrays that can be determined at compile time may be allocated directly in stone memory.
|
||||
|
||||
## Relationship to GC
|
||||
|
||||
The Cheney copying collector only operates on the mutable heap. During collection, when the collector encounters a pointer to stone memory (S bit set), it skips it — stone objects are roots that never move. This means stone memory acts as a permanent root set with zero GC overhead.
|
||||
96
docs/spec/values.md
Normal file
96
docs/spec/values.md
Normal file
@@ -0,0 +1,96 @@
|
||||
---
|
||||
title: "Value Representation"
|
||||
description: "JSValue tagging and encoding"
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Every value in ƿit is a 64-bit word called a JSValue. The runtime uses LSB (least significant bit) tagging to pack type information directly into the value, avoiding heap allocation for common types.
|
||||
|
||||
## Tag Encoding
|
||||
|
||||
The lowest bits of a JSValue determine its type:
|
||||
|
||||
| LSB Pattern | Type | Payload |
|
||||
|-------------|------|---------|
|
||||
| `xxxxxxx0` | Integer | 31-bit signed integer in upper bits |
|
||||
| `xxxxx001` | Pointer | 61-bit aligned heap pointer |
|
||||
| `xxxxx101` | Short float | 8-bit exponent + 52-bit mantissa |
|
||||
| `xxxxx011` | Special | 5-bit tag selects subtype |
|
||||
|
||||
### Integers
|
||||
|
||||
If the least significant bit is 0, the value is an immediate 31-bit signed integer. The integer is stored in the upper bits, extracted via `v >> 1`.
|
||||
|
||||
```
|
||||
[integer: 31 bits][0]
|
||||
```
|
||||
|
||||
Range: -1073741824 to 1073741823. Numbers outside this range are stored as short floats or heap-allocated.
|
||||
|
||||
### Pointers
|
||||
|
||||
If the lowest 3 bits are `001`, the value is a pointer to a heap object. The pointer is 8-byte aligned, so the low 3 bits are available for the tag. The actual address is extracted by clearing the low 3 bits.
|
||||
|
||||
```
|
||||
[pointer: 61 bits][001]
|
||||
```
|
||||
|
||||
All heap objects (arrays, records, blobs, text, functions, etc.) are referenced through pointer-tagged JSValues.
|
||||
|
||||
### Short Floats
|
||||
|
||||
If the lowest 3 bits are `101`, the value encodes a floating-point number directly. The format uses an 8-bit exponent (bias 127) and 52-bit mantissa, similar to IEEE 754 but with reduced range.
|
||||
|
||||
```
|
||||
[sign: 1][exponent: 8][mantissa: 52][101]
|
||||
```
|
||||
|
||||
Range: approximately ±3.4 * 10^38. Numbers outside this range fall back to null. Zero is always positive zero.
|
||||
|
||||
### Specials
|
||||
|
||||
If the lowest 2 bits are `11`, the next 3 bits select a special type:
|
||||
|
||||
| 5-bit Tag | Value |
|
||||
|-----------|-------|
|
||||
| `00011` | Boolean (true/false in upper bits) |
|
||||
| `00111` | Null |
|
||||
| `01111` | Exception marker |
|
||||
| `10111` | Uninitialized |
|
||||
| `11011` | Immediate string |
|
||||
| `11111` | Catch offset |
|
||||
|
||||
## Immediate Strings
|
||||
|
||||
Short ASCII strings (up to 7 characters) are packed directly into the JSValue without heap allocation:
|
||||
|
||||
```
|
||||
[char6][char5][char4][char3][char2][char1][char0][length: 3][11011]
|
||||
```
|
||||
|
||||
Each character occupies 8 bits. The length (0-7) is stored in bits 5-7. Only ASCII characters (0-127) qualify — any non-ASCII character forces heap allocation.
|
||||
|
||||
```javascript
|
||||
var s = "hello" // 5 chars, fits in immediate string
|
||||
var t = "" // immediate (length 0)
|
||||
var u = "longtext" // 8 chars, heap-allocated
|
||||
```
|
||||
|
||||
## Null
|
||||
|
||||
Null is encoded as a special-tagged value with tag `00111`. There is no `undefined` in ƿit — only null.
|
||||
|
||||
```javascript
|
||||
var x = null // special tag null
|
||||
var y = 1 / 0 // also null (division by zero)
|
||||
var z = {}.missing // null (missing field)
|
||||
```
|
||||
|
||||
## Boolean
|
||||
|
||||
True and false are encoded as specials with tag `00011`, distinguished by a bit in the upper payload.
|
||||
|
||||
## Summary
|
||||
|
||||
The tagging scheme ensures that the most common values — small integers, booleans, null, and short strings — require zero heap allocation. This significantly reduces GC pressure and improves cache locality.
|
||||
119
docs/wota.md
Normal file
119
docs/wota.md
Normal file
@@ -0,0 +1,119 @@
|
||||
---
|
||||
title: "Wota Format"
|
||||
description: "Word Object Transfer Arrangement"
|
||||
weight: 86
|
||||
type: "docs"
|
||||
---
|
||||
|
||||
Wota is a binary message format for local inter-process communication. It is similar to Nota but works at word granularity (64-bit words) rather than byte granularity. Wota arrangements are less compact than Nota but faster to arrange and consume.
|
||||
|
||||
Wota stands for Word Object Transfer Arrangement.
|
||||
|
||||
## Type Summary
|
||||
|
||||
| Byte | Type |
|
||||
|------|------|
|
||||
| `00` | Integer |
|
||||
| `01` | Floating Point |
|
||||
| `02` | Array |
|
||||
| `03` | Record |
|
||||
| `04` | Blob |
|
||||
| `05` | Text |
|
||||
| `07` | Symbol |
|
||||
|
||||
## Preambles
|
||||
|
||||
Every Wota value starts with a preamble word. The least significant byte contains the type. The remaining 56 bits contain type-specific data.
|
||||
|
||||
## Blob
|
||||
|
||||
A blob is a string of bits. The remaining field contains the number of bits. The number of words that follow: `floor((number_of_bits + 63) / 64)`. The first bit of the blob goes into the most significant bit of the first word. The final word is padded with 0.
|
||||
|
||||
Example: A blob containing 25 bits `111100001110001100100001`:
|
||||
|
||||
```
|
||||
0000000000001904 # preamble: 25 bits, type blob
|
||||
F0E3208000000000 # data (padded to 64 bits)
|
||||
```
|
||||
|
||||
## Text
|
||||
|
||||
The text is a string of UTF-32 characters packed 2 per word. The remaining field contains the number of characters. The number of words that follow: `floor((number_of_characters + 1) / 2)`. The final word is padded with 0.
|
||||
|
||||
Example: `"cat"`:
|
||||
|
||||
```
|
||||
0000000000000305 # preamble: 3 characters, type text
|
||||
0000006300000061 # 'c' and 'a'
|
||||
0000007400000000 # 't' and padding
|
||||
```
|
||||
|
||||
## Array
|
||||
|
||||
An array is an ordered sequence of values. The remaining field contains the number of elements. Following the preamble are the elements, each beginning with its own preamble. Nesting is encouraged. Cyclic structures are not allowed.
|
||||
|
||||
Example: `["duck", "dragon"]`:
|
||||
|
||||
```
|
||||
0000000000000202 # preamble: 2 elements, type array
|
||||
0000000000000405 # text "duck": 4 chars
|
||||
0000006400000074 # 'd' 't' (reversed pair order)
|
||||
000000630000006B # 'c' 'k'
|
||||
0000000000000605 # text "dragon": 6 chars
|
||||
0000006400000072 # 'd' 'r'
|
||||
0000006100000067 # 'a' 'g'
|
||||
0000006F0000006E # 'o' 'n'
|
||||
```
|
||||
|
||||
## Record
|
||||
|
||||
A record is a set of key/value pairs. Keys must be text. The remaining field contains the number of pairs.
|
||||
|
||||
Example: `{"ox": ["O", "X"]}`:
|
||||
|
||||
```
|
||||
0000000000000103 # preamble: 1 pair, type record
|
||||
0000000000000205 # key "ox": 2 chars
|
||||
0000006F00000078 # 'o' 'x'
|
||||
0000000000000202 # value: array of 2
|
||||
0000000000000105 # "O": 1 char
|
||||
0000004F00000000 # 'O'
|
||||
0000000000000105 # "X": 1 char
|
||||
0000005800000000 # 'X'
|
||||
```
|
||||
|
||||
## Number
|
||||
|
||||
Numbers are represented as DEC64. To arrange an integer, shift the integer up 8 bits. The number is incorporated directly into the preamble.
|
||||
|
||||
Example: `7`:
|
||||
|
||||
```
|
||||
0000000000000700 # integer 7 as DEC64
|
||||
```
|
||||
|
||||
To arrange a floating point number, place the number in the word following the floating point preamble.
|
||||
|
||||
Example: `4.25`:
|
||||
|
||||
```
|
||||
0000000000000001 # preamble: type floating point
|
||||
000000000001A9FE # DEC64 encoding of 4.25
|
||||
```
|
||||
|
||||
Care must be taken when decoding that the least significant byte of the number is not `80` (the null exponent).
|
||||
|
||||
## Symbol
|
||||
|
||||
The remaining field contains the symbol.
|
||||
|
||||
Example: `[null, false, true, private, system]`:
|
||||
|
||||
```
|
||||
0000000000000502 # array of 5
|
||||
0000000000000007 # null
|
||||
0000000000000207 # false
|
||||
0000000000000307 # true
|
||||
0000000000000807 # private
|
||||
0000000000000907 # system
|
||||
```
|
||||
251
editors/ai/pit-context.md
Normal file
251
editors/ai/pit-context.md
Normal file
@@ -0,0 +1,251 @@
|
||||
# ƿit Language — AI Context
|
||||
|
||||
ƿit (pronounced "pit") is a safe, actor-based programming language. Its syntax resembles JavaScript but with significant differences. Scripts use `.ce` (actors) and `.cm` (modules) file extensions.
|
||||
|
||||
## Key Differences from JavaScript
|
||||
|
||||
- **`var` / `def`** — `var` is mutable, `def` is constant. No `let` or `const`.
|
||||
- **`==` is strict** — No `===` or `!==`. `==` and `!=` are always strict comparison.
|
||||
- **No `undefined`** — Only `null`. Division by zero produces `null`, not `Infinity`.
|
||||
- **No classes** — Use `meme()`, `proto()`, `isa()` for prototype chains.
|
||||
- **No `for...in`, `for...of`, spread, rest, or default params.**
|
||||
- **Variables declared at function body level only** — Not inside `if`/`while`/`for` blocks.
|
||||
- **All variables must be initialized** — `var x` alone is an error; use `var x = null`.
|
||||
- **`disrupt` / `disruption`** — No `try`/`catch`/`throw`. Error handling uses:
|
||||
```javascript
|
||||
var fn = function() {
|
||||
disrupt // raise an error (bare keyword, no value)
|
||||
} disruption {
|
||||
// handle the error
|
||||
}
|
||||
```
|
||||
- **No arraybuffers** — Use `blob` (works with bits; `stone(blob)` before reading).
|
||||
- **Identifiers can contain `?` and `!`** — e.g., `nil?`, `set!`, `is?valid`.
|
||||
- **4-parameter limit** — Functions take at most 4 named parameters.
|
||||
- **Everything lowercase** — Convention is all-lowercase identifiers with underscores.
|
||||
|
||||
## Variable Declaration
|
||||
|
||||
```javascript
|
||||
var count = 0 // mutable
|
||||
def MAX = 100 // constant (cannot be reassigned)
|
||||
var x = null // must initialize (var x alone is an error)
|
||||
```
|
||||
|
||||
## Functions
|
||||
|
||||
```javascript
|
||||
var greet = function(name) {
|
||||
print(`hello ${name}`)
|
||||
}
|
||||
|
||||
// Arrow functions
|
||||
var double = x => x * 2
|
||||
var add = (a, b) => a + b
|
||||
```
|
||||
|
||||
## Push / Pop Syntax
|
||||
|
||||
```javascript
|
||||
var a = [1, 2]
|
||||
a[] = 3 // push: a is now [1, 2, 3]
|
||||
var v = a[] // pop: v is 3, a is [1, 2]
|
||||
```
|
||||
|
||||
## Control Flow
|
||||
|
||||
```javascript
|
||||
if (x > 0) {
|
||||
print("positive")
|
||||
} else {
|
||||
print("non-positive")
|
||||
}
|
||||
|
||||
while (i < 10) {
|
||||
i = i + 1
|
||||
}
|
||||
|
||||
for (var i = 0; i < 10; i = i + 1) {
|
||||
print(i)
|
||||
}
|
||||
|
||||
// do-while
|
||||
do {
|
||||
i = i + 1
|
||||
} while (i < 10)
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
```javascript
|
||||
var safe_divide = function(a, b) {
|
||||
if (b == 0) {
|
||||
disrupt
|
||||
}
|
||||
return a / b
|
||||
} disruption {
|
||||
return null
|
||||
}
|
||||
```
|
||||
|
||||
## Creator Functions (Polymorphic)
|
||||
|
||||
These examine argument types to decide behavior:
|
||||
|
||||
### array()
|
||||
- `array(5)` — `[null, null, null, null, null]`
|
||||
- `array(3, 0)` — `[0, 0, 0]`
|
||||
- `array(5, i => i * 2)` — `[0, 2, 4, 6, 8]`
|
||||
- `array([1,2])` — copy
|
||||
- `array([1,2,3], x => x * 10)` — map: `[10, 20, 30]`
|
||||
- `array([1,2], [3,4])` — concat: `[1, 2, 3, 4]`
|
||||
- `array([1,2,3,4,5], 1, 4)` — slice: `[2, 3, 4]`
|
||||
- `array({a: 1, b: 2})` — keys: `["a", "b"]`
|
||||
- `array("hello")` — characters: `["h", "e", "l", "l", "o"]`
|
||||
- `array("a,b,c", ",")` — split: `["a", "b", "c"]`
|
||||
|
||||
### text()
|
||||
- `text([1, 2, 3], ", ")` — join: `"1, 2, 3"`
|
||||
- `text(255, 16)` — radix: `"ff"`
|
||||
- `text("hello", 0, 3)` — substring: `"hel"`
|
||||
|
||||
### number()
|
||||
- `number("42")` — parse: `42`
|
||||
- `number("ff", 16)` — radix: `255`
|
||||
- `number(true)` — `1`
|
||||
|
||||
### record()
|
||||
- `record({a: 1})` — copy
|
||||
- `record({a: 1}, {b: 2})` — merge: `{a: 1, b: 2}`
|
||||
- `record(["x", "y"])` — from keys: `{x: true, y: true}`
|
||||
|
||||
## All Intrinsic Functions
|
||||
|
||||
**Constants:** `false`, `true`, `null`, `pi`
|
||||
|
||||
**Type checks:** `is_array`, `is_blob`, `is_character`, `is_data`, `is_digit`, `is_false`, `is_fit`, `is_function`, `is_integer`, `is_letter`, `is_logical`, `is_lower`, `is_null`, `is_number`, `is_object`, `is_pattern`, `is_stone`, `is_text`, `is_true`, `is_upper`, `is_whitespace`
|
||||
|
||||
**Creators:** `array`, `logical`, `number`, `record`, `text`
|
||||
|
||||
**Math:** `abs`, `ceiling`, `floor`, `fraction`, `max`, `min`, `modulo`, `neg`, `remainder`, `round`, `sign`, `trunc`, `whole`
|
||||
|
||||
**Text:** `character`, `codepoint`, `ends_with`, `extract`, `format`, `lower`, `normalize`, `replace`, `search`, `starts_with`, `trim`, `upper`
|
||||
|
||||
**Array:** `every`, `filter`, `find`, `for`, `length`, `reduce`, `reverse`, `some`, `sort`
|
||||
|
||||
**Objects:** `meme`, `proto`, `isa`, `stone`
|
||||
|
||||
**Functions:** `apply`, `splat`
|
||||
|
||||
**I/O:** `print`
|
||||
|
||||
**Async:** `fallback`, `parallel`, `race`, `sequence`
|
||||
|
||||
**Misc:** `logical`, `not`, `use`
|
||||
|
||||
## Variable Scoping
|
||||
|
||||
Variables are scoped to the function body in which they are declared. There is no block scoping. All declarations must be at the top level of a function body (not nested inside `if`/`while`/`for`).
|
||||
|
||||
```javascript
|
||||
var outer = function() {
|
||||
var x = 10
|
||||
var inner = function() {
|
||||
// x is visible here via closure
|
||||
print(x)
|
||||
}
|
||||
inner()
|
||||
}
|
||||
```
|
||||
|
||||
## Modules (.cm files)
|
||||
|
||||
Modules return a value (typically a record of exports). They are loaded with `use()`, cached, and frozen.
|
||||
|
||||
```javascript
|
||||
// math_utils.cm
|
||||
var square = x => x * x
|
||||
var cube = x => x * x * x
|
||||
return {square: square, cube: cube}
|
||||
|
||||
// main.ce
|
||||
var utils = use('math_utils')
|
||||
print(utils.square(5)) // 25
|
||||
```
|
||||
|
||||
## Standard Library (loaded with use())
|
||||
|
||||
- `blob` — binary data (works with bits, not bytes)
|
||||
- `time` — time constants and conversions
|
||||
- `math` — trig, logarithms, roots (sub-modules: `math/radians`, `math/turns`)
|
||||
- `json` — JSON encoding/decoding (`json.encode`, `json.decode`)
|
||||
- `random` — random number generation
|
||||
- `fd` — file descriptor operations (`fd.read`, `fd.write`, `fd.slurp`, `fd.stat`)
|
||||
|
||||
## Actor Model (.ce files)
|
||||
|
||||
Actors are independent execution units that never share memory. They communicate via message passing.
|
||||
|
||||
```javascript
|
||||
// greeter.ce
|
||||
$receiver(function(msg) {
|
||||
$send(msg.from, {greeting: `hello ${msg.name}`})
|
||||
})
|
||||
```
|
||||
|
||||
### Actor Intrinsics ($ prefix)
|
||||
|
||||
- `$me` — this actor's address
|
||||
- `$send(address, message)` — send a message
|
||||
- `$start(script, env)` — start a new actor
|
||||
- `$stop()` — stop this actor
|
||||
- `$delay(ms)` — delay processing
|
||||
- `$receiver(fn)` — set message handler
|
||||
- `$clock(interval, message)` — periodic self-message
|
||||
- `$portal(name)` — create named portal
|
||||
- `$contact(name)` — connect to portal
|
||||
- `$couple(address)` — lifecycle coupling
|
||||
- `$unneeded(fn)` — cleanup callback
|
||||
- `$connection(address)` — establish connection
|
||||
- `$time_limit(ms)` — execution time limit
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Iteration
|
||||
```javascript
|
||||
// Preferred: use for() intrinsic
|
||||
for([1, 2, 3], function(item, index) {
|
||||
print(`${text(index)}: ${text(item)}`)
|
||||
})
|
||||
|
||||
// C-style for loop
|
||||
for (var i = 0; i < length(items); i = i + 1) {
|
||||
print(items[i])
|
||||
}
|
||||
```
|
||||
|
||||
### String Building
|
||||
```javascript
|
||||
// Use backtick interpolation
|
||||
var msg = `hello ${name}, you are ${text(age)} years old`
|
||||
|
||||
// Join array
|
||||
var csv = text(values, ",")
|
||||
```
|
||||
|
||||
### Record Manipulation
|
||||
```javascript
|
||||
var obj = {name: "alice", age: 30}
|
||||
var keys = array(obj) // ["name", "age"]
|
||||
var copy = record(obj) // mutable copy
|
||||
var merged = record(obj, {role: "admin"})
|
||||
```
|
||||
|
||||
### Error-Safe Operations
|
||||
```javascript
|
||||
var safe_parse = function(input) {
|
||||
return number(input)
|
||||
} disruption {
|
||||
return null
|
||||
}
|
||||
```
|
||||
30
editors/vscode/language-configuration.json
Normal file
30
editors/vscode/language-configuration.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"comments": {
|
||||
"lineComment": "//",
|
||||
"blockComment": ["/*", "*/"]
|
||||
},
|
||||
"brackets": [
|
||||
["{", "}"],
|
||||
["[", "]"],
|
||||
["(", ")"]
|
||||
],
|
||||
"autoClosingPairs": [
|
||||
{ "open": "{", "close": "}" },
|
||||
{ "open": "[", "close": "]" },
|
||||
{ "open": "(", "close": ")" },
|
||||
{ "open": "\"", "close": "\"", "notIn": ["string"] },
|
||||
{ "open": "`", "close": "`", "notIn": ["string"] }
|
||||
],
|
||||
"surroundingPairs": [
|
||||
["{", "}"],
|
||||
["[", "]"],
|
||||
["(", ")"],
|
||||
["\"", "\""],
|
||||
["`", "`"]
|
||||
],
|
||||
"indentationRules": {
|
||||
"increaseIndentPattern": "^.*\\{[^}\"'`]*$",
|
||||
"decreaseIndentPattern": "^\\s*\\}"
|
||||
},
|
||||
"wordPattern": "[a-zA-Z_$][a-zA-Z0-9_$?!]*"
|
||||
}
|
||||
113
editors/vscode/lsp/analysis.cm
Normal file
113
editors/vscode/lsp/analysis.cm
Normal file
@@ -0,0 +1,113 @@
|
||||
// Document analysis module.
|
||||
// Call make(tokenize_mod, parse_mod) to get an analysis object.
|
||||
|
||||
var json = use('json')
|
||||
|
||||
// Create an analysis module bound to the tokenize and parse functions.
|
||||
var make = function(tokenize_mod, parse_mod) {
|
||||
|
||||
// Tokenize and parse a document, storing the results.
|
||||
var update = function(docs, uri, params) {
|
||||
var src = params.src
|
||||
var version = params.version
|
||||
var tok_result = null
|
||||
var ast = null
|
||||
var errors = []
|
||||
var doc = null
|
||||
|
||||
var do_tokenize = function() {
|
||||
tok_result = tokenize_mod(src, uri)
|
||||
} disruption {
|
||||
errors = [{message: "Tokenize failed", line: 1, column: 1}]
|
||||
}
|
||||
var do_parse = function() {
|
||||
ast = parse_mod(tok_result.tokens, src, uri, tokenize_mod)
|
||||
} disruption {
|
||||
// parse_mod may set errors on ast even on partial failure
|
||||
}
|
||||
|
||||
do_tokenize()
|
||||
|
||||
if (tok_result != null) {
|
||||
do_parse()
|
||||
|
||||
if (ast != null && ast.errors != null) {
|
||||
errors = ast.errors
|
||||
}
|
||||
}
|
||||
|
||||
doc = {
|
||||
uri: uri,
|
||||
text: src,
|
||||
version: version,
|
||||
tokens: (tok_result != null) ? tok_result.tokens : [],
|
||||
ast: ast,
|
||||
errors: errors
|
||||
}
|
||||
docs[uri] = doc
|
||||
return doc
|
||||
}
|
||||
|
||||
// Remove a document from the store.
|
||||
var remove = function(docs, uri) {
|
||||
delete docs[uri]
|
||||
}
|
||||
|
||||
// Convert parse errors to LSP diagnostics.
|
||||
var diagnostics = function(doc) {
|
||||
var result = []
|
||||
var _i = 0
|
||||
var e = null
|
||||
var line = null
|
||||
var col = null
|
||||
while (_i < length(doc.errors)) {
|
||||
e = doc.errors[_i]
|
||||
line = (e.line != null) ? e.line - 1 : 0
|
||||
col = (e.column != null) ? e.column - 1 : 0
|
||||
result[] = {
|
||||
range: {
|
||||
start: {line: line, character: col},
|
||||
end: {line: line, character: col + 1}
|
||||
},
|
||||
severity: 1,
|
||||
source: "pit",
|
||||
message: e.message
|
||||
}
|
||||
_i = _i + 1
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Find the token at a given line/column (0-based).
|
||||
var token_at = function(doc, line, col) {
|
||||
var tokens = doc.tokens
|
||||
var _i = 0
|
||||
var tok = null
|
||||
while (_i < length(tokens)) {
|
||||
tok = tokens[_i]
|
||||
if (tok.from_row == line && tok.from_column <= col && tok.to_column >= col) {
|
||||
return tok
|
||||
}
|
||||
if (tok.from_row < line && tok.to_row > line) {
|
||||
return tok
|
||||
}
|
||||
if (tok.from_row < line && tok.to_row == line && tok.to_column >= col) {
|
||||
return tok
|
||||
}
|
||||
if (tok.from_row == line && tok.to_row > line && tok.from_column <= col) {
|
||||
return tok
|
||||
}
|
||||
_i = _i + 1
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
update: update,
|
||||
remove: remove,
|
||||
diagnostics: diagnostics,
|
||||
token_at: token_at
|
||||
}
|
||||
}
|
||||
|
||||
return make
|
||||
133
editors/vscode/lsp/completions.cm
Normal file
133
editors/vscode/lsp/completions.cm
Normal file
@@ -0,0 +1,133 @@
|
||||
// Completion provider for the ƿit LSP.
|
||||
|
||||
// CompletionItemKind constants (LSP spec)
|
||||
def KIND_FUNCTION = 3
|
||||
def KIND_VARIABLE = 6
|
||||
def KIND_KEYWORD = 14
|
||||
def KIND_CONSTANT = 21
|
||||
|
||||
// All intrinsic function names
|
||||
def intrinsic_functions = [
|
||||
"abs", "apply", "array", "ceiling", "character", "codepoint",
|
||||
"ends_with", "every", "extract", "fallback", "filter", "find",
|
||||
"floor", "format", "fraction",
|
||||
"is_array", "is_blob", "is_character", "is_data", "is_digit",
|
||||
"is_false", "is_fit", "is_function", "is_integer", "is_letter",
|
||||
"is_logical", "is_lower", "is_null", "is_number", "is_object",
|
||||
"is_pattern", "is_stone", "is_text", "is_true", "is_upper",
|
||||
"is_whitespace",
|
||||
"length", "logical", "lower", "max", "min", "modulo",
|
||||
"neg", "normalize", "not", "number",
|
||||
"parallel", "print", "race", "record", "reduce", "remainder",
|
||||
"replace", "reverse", "round",
|
||||
"search", "sequence", "sign", "some", "sort", "starts_with",
|
||||
"stone", "text", "trim", "trunc", "upper", "whole",
|
||||
"meme", "proto", "isa", "splat", "use"
|
||||
]
|
||||
|
||||
// Keywords that can be completed
|
||||
def keywords = [
|
||||
"var", "def", "if", "else", "for", "while", "do",
|
||||
"function", "return", "go", "break", "continue",
|
||||
"disrupt", "disruption", "delete", "in", "this",
|
||||
"null", "true", "false"
|
||||
]
|
||||
|
||||
// Actor intrinsics (only in .ce files)
|
||||
def actor_intrinsics = [
|
||||
"$me", "$send", "$start", "$stop", "$delay",
|
||||
"$receiver", "$clock", "$portal", "$contact",
|
||||
"$couple", "$unneeded", "$connection", "$time_limit"
|
||||
]
|
||||
|
||||
// Walk AST scopes to find variables visible at a position.
|
||||
var collect_scope_vars = function(doc, line, col) {
|
||||
var vars = []
|
||||
var ast = doc.ast
|
||||
var _i = 0
|
||||
var _j = 0
|
||||
var scope = null
|
||||
var v = null
|
||||
|
||||
if (ast == null || ast.scopes == null) {
|
||||
return vars
|
||||
}
|
||||
|
||||
// Collect variables from all scopes (simplified: return all declared vars)
|
||||
while (_i < length(ast.scopes)) {
|
||||
scope = ast.scopes[_i]
|
||||
if (scope.vars != null) {
|
||||
_j = 0
|
||||
while (_j < length(scope.vars)) {
|
||||
v = scope.vars[_j]
|
||||
if (v.name != null) {
|
||||
vars[] = {
|
||||
label: v.name,
|
||||
kind: (v.is_const == true) ? KIND_CONSTANT : KIND_VARIABLE,
|
||||
detail: (v.is_const == true) ? "def" : "var"
|
||||
}
|
||||
}
|
||||
_j = _j + 1
|
||||
}
|
||||
}
|
||||
_i = _i + 1
|
||||
}
|
||||
|
||||
return vars
|
||||
}
|
||||
|
||||
// Provide completions for a document at a position.
|
||||
var complete = function(doc, line, col) {
|
||||
var items = []
|
||||
var _i = 0
|
||||
var is_actor = ends_with(doc.uri, ".ce")
|
||||
|
||||
// Intrinsic functions
|
||||
_i = 0
|
||||
while (_i < length(intrinsic_functions)) {
|
||||
items[] = {
|
||||
label: intrinsic_functions[_i],
|
||||
kind: KIND_FUNCTION,
|
||||
detail: "intrinsic"
|
||||
}
|
||||
_i = _i + 1
|
||||
}
|
||||
|
||||
// Keywords
|
||||
_i = 0
|
||||
while (_i < length(keywords)) {
|
||||
items[] = {
|
||||
label: keywords[_i],
|
||||
kind: KIND_KEYWORD,
|
||||
detail: "keyword"
|
||||
}
|
||||
_i = _i + 1
|
||||
}
|
||||
|
||||
// Actor intrinsics (only for .ce files)
|
||||
if (is_actor) {
|
||||
_i = 0
|
||||
while (_i < length(actor_intrinsics)) {
|
||||
items[] = {
|
||||
label: actor_intrinsics[_i],
|
||||
kind: KIND_FUNCTION,
|
||||
detail: "actor intrinsic"
|
||||
}
|
||||
_i = _i + 1
|
||||
}
|
||||
}
|
||||
|
||||
// Variables from scope analysis
|
||||
var scope_vars = collect_scope_vars(doc, line, col)
|
||||
_i = 0
|
||||
while (_i < length(scope_vars)) {
|
||||
items[] = scope_vars[_i]
|
||||
_i = _i + 1
|
||||
}
|
||||
|
||||
return items
|
||||
}
|
||||
|
||||
return {
|
||||
complete: complete
|
||||
}
|
||||
461
editors/vscode/lsp/hover.cm
Normal file
461
editors/vscode/lsp/hover.cm
Normal file
@@ -0,0 +1,461 @@
|
||||
// Hover provider for the ƿit LSP.
|
||||
// Shows documentation for intrinsic functions and variable info.
|
||||
|
||||
// Intrinsic function documentation database.
|
||||
// Each entry: {signature, description}
|
||||
def intrinsic_docs = {
|
||||
abs: {
|
||||
signature: "abs(number)",
|
||||
description: "Absolute value. Returns null for non-numbers."
|
||||
},
|
||||
apply: {
|
||||
signature: "apply(function, array)",
|
||||
description: "Execute the function, passing array elements as input values."
|
||||
},
|
||||
array: {
|
||||
signature: "array(value, ...)",
|
||||
description: "Create arrays. Polymorphic: array(number) creates sized array, array(array) copies, array(array, fn) maps, array(text) splits into characters, array(text, sep) splits by separator."
|
||||
},
|
||||
ceiling: {
|
||||
signature: "ceiling(number, place)",
|
||||
description: "Round up. If place is 0 or null, round to smallest integer >= number."
|
||||
},
|
||||
character: {
|
||||
signature: "character(value)",
|
||||
description: "If text, returns the first character. If a non-negative integer, returns the character from that codepoint."
|
||||
},
|
||||
codepoint: {
|
||||
signature: "codepoint(text)",
|
||||
description: "Returns the codepoint number of the first character."
|
||||
},
|
||||
ends_with: {
|
||||
signature: "ends_with(text, suffix)",
|
||||
description: "Returns true if the text ends with the given suffix."
|
||||
},
|
||||
every: {
|
||||
signature: "every(array, function)",
|
||||
description: "Returns true if every element satisfies the predicate."
|
||||
},
|
||||
extract: {
|
||||
signature: "extract(text, pattern, from, to)",
|
||||
description: "Match text to pattern. Returns a record of saved fields, or null if no match."
|
||||
},
|
||||
fallback: {
|
||||
signature: "fallback(requestor_array)",
|
||||
description: "Returns a requestor that tries each requestor in order until one succeeds."
|
||||
},
|
||||
filter: {
|
||||
signature: "filter(array, function)",
|
||||
description: "Returns a new array containing elements for which function returns true."
|
||||
},
|
||||
find: {
|
||||
signature: "find(array, function, reverse, from)",
|
||||
description: "Returns the element number where function returns true, or null if not found. If second arg is not a function, compares directly."
|
||||
},
|
||||
floor: {
|
||||
signature: "floor(number, place)",
|
||||
description: "Round down. If place is 0 or null, round to greatest integer <= number."
|
||||
},
|
||||
format: {
|
||||
signature: "format(text, collection, transformer)",
|
||||
description: "Substitute {key} placeholders in text with values from a collection (array or record)."
|
||||
},
|
||||
fraction: {
|
||||
signature: "fraction(number)",
|
||||
description: "Returns the fractional part of a number."
|
||||
},
|
||||
is_array: {
|
||||
signature: "is_array(value)",
|
||||
description: "Returns true if the value is an array."
|
||||
},
|
||||
is_blob: {
|
||||
signature: "is_blob(value)",
|
||||
description: "Returns true if the value is a blob."
|
||||
},
|
||||
is_character: {
|
||||
signature: "is_character(value)",
|
||||
description: "Returns true if the value is a single character."
|
||||
},
|
||||
is_data: {
|
||||
signature: "is_data(value)",
|
||||
description: "Returns true if the value is data (not a function)."
|
||||
},
|
||||
is_digit: {
|
||||
signature: "is_digit(value)",
|
||||
description: "Returns true if the value is a digit character."
|
||||
},
|
||||
is_false: {
|
||||
signature: "is_false(value)",
|
||||
description: "Returns true if the value is false."
|
||||
},
|
||||
is_fit: {
|
||||
signature: "is_fit(value)",
|
||||
description: "Returns true if the value is a fit integer."
|
||||
},
|
||||
is_function: {
|
||||
signature: "is_function(value)",
|
||||
description: "Returns true if the value is a function."
|
||||
},
|
||||
is_integer: {
|
||||
signature: "is_integer(value)",
|
||||
description: "Returns true if the value is an integer."
|
||||
},
|
||||
is_letter: {
|
||||
signature: "is_letter(value)",
|
||||
description: "Returns true if the value is a letter character."
|
||||
},
|
||||
is_logical: {
|
||||
signature: "is_logical(value)",
|
||||
description: "Returns true if the value is a logical (boolean)."
|
||||
},
|
||||
is_lower: {
|
||||
signature: "is_lower(value)",
|
||||
description: "Returns true if the value is a lowercase character."
|
||||
},
|
||||
is_null: {
|
||||
signature: "is_null(value)",
|
||||
description: "Returns true if the value is null."
|
||||
},
|
||||
is_number: {
|
||||
signature: "is_number(value)",
|
||||
description: "Returns true if the value is a number."
|
||||
},
|
||||
is_object: {
|
||||
signature: "is_object(value)",
|
||||
description: "Returns true if the value is an object (record)."
|
||||
},
|
||||
is_pattern: {
|
||||
signature: "is_pattern(value)",
|
||||
description: "Returns true if the value is a pattern (regex)."
|
||||
},
|
||||
is_stone: {
|
||||
signature: "is_stone(value)",
|
||||
description: "Returns true if the value is frozen (stoned)."
|
||||
},
|
||||
is_text: {
|
||||
signature: "is_text(value)",
|
||||
description: "Returns true if the value is text."
|
||||
},
|
||||
is_true: {
|
||||
signature: "is_true(value)",
|
||||
description: "Returns true if the value is true."
|
||||
},
|
||||
is_upper: {
|
||||
signature: "is_upper(value)",
|
||||
description: "Returns true if the value is an uppercase character."
|
||||
},
|
||||
is_whitespace: {
|
||||
signature: "is_whitespace(value)",
|
||||
description: "Returns true if the value is a whitespace character."
|
||||
},
|
||||
length: {
|
||||
signature: "length(value)",
|
||||
description: "Array: number of elements. Text: number of codepoints. Function: arity. Blob: number of bits. Record: record.length()."
|
||||
},
|
||||
logical: {
|
||||
signature: "logical(value)",
|
||||
description: "Convert to logical. 0/false/null/\"false\" produce false; 1/true/\"true\" produce true."
|
||||
},
|
||||
lower: {
|
||||
signature: "lower(text)",
|
||||
description: "Returns text with all uppercase characters converted to lowercase."
|
||||
},
|
||||
max: {
|
||||
signature: "max(number, number)",
|
||||
description: "Returns the larger of two numbers."
|
||||
},
|
||||
min: {
|
||||
signature: "min(number, number)",
|
||||
description: "Returns the smaller of two numbers."
|
||||
},
|
||||
modulo: {
|
||||
signature: "modulo(dividend, divisor)",
|
||||
description: "Result has the sign of the divisor."
|
||||
},
|
||||
neg: {
|
||||
signature: "neg(number)",
|
||||
description: "Negate. Reverse the sign of a number."
|
||||
},
|
||||
normalize: {
|
||||
signature: "normalize(text)",
|
||||
description: "Unicode normalize."
|
||||
},
|
||||
not: {
|
||||
signature: "not(logical)",
|
||||
description: "Returns the opposite logical. Returns null for non-logicals."
|
||||
},
|
||||
number: {
|
||||
signature: "number(value, radix_or_format)",
|
||||
description: "Convert to number. Polymorphic: number(logical), number(text), number(text, radix), number(text, format)."
|
||||
},
|
||||
parallel: {
|
||||
signature: "parallel(requestor_array, throttle, need)",
|
||||
description: "Start all requestors concurrently. Optional throttle limits concurrency; optional need specifies minimum successes."
|
||||
},
|
||||
print: {
|
||||
signature: "print(value)",
|
||||
description: "Print a value to standard output."
|
||||
},
|
||||
race: {
|
||||
signature: "race(requestor_array, throttle, need)",
|
||||
description: "Like parallel but returns as soon as needed results are obtained. Default need is 1."
|
||||
},
|
||||
record: {
|
||||
signature: "record(value, ...)",
|
||||
description: "Create records. Polymorphic: record(record) copies, record(record, record) merges, record(array) creates from keys."
|
||||
},
|
||||
reduce: {
|
||||
signature: "reduce(array, function, initial, reverse)",
|
||||
description: "Reduce an array to a single value by applying a function to pairs of elements."
|
||||
},
|
||||
remainder: {
|
||||
signature: "remainder(dividend, divisor)",
|
||||
description: "For fit integers: dividend - ((dividend // divisor) * divisor)."
|
||||
},
|
||||
replace: {
|
||||
signature: "replace(text, target, replacement, limit)",
|
||||
description: "Return text with target replaced. Target can be text or pattern. Replacement can be text or function."
|
||||
},
|
||||
reverse: {
|
||||
signature: "reverse(array)",
|
||||
description: "Returns a new array with elements in the opposite order."
|
||||
},
|
||||
round: {
|
||||
signature: "round(number, place)",
|
||||
description: "Round to nearest."
|
||||
},
|
||||
search: {
|
||||
signature: "search(text, target, from)",
|
||||
description: "Search text for target. Returns character position or null."
|
||||
},
|
||||
sequence: {
|
||||
signature: "sequence(requestor_array)",
|
||||
description: "Process requestors in order. Each result becomes input to the next."
|
||||
},
|
||||
sign: {
|
||||
signature: "sign(number)",
|
||||
description: "Returns -1, 0, or 1."
|
||||
},
|
||||
some: {
|
||||
signature: "some(array, function)",
|
||||
description: "Returns true if any element satisfies the predicate."
|
||||
},
|
||||
sort: {
|
||||
signature: "sort(array, select)",
|
||||
description: "Returns a new sorted array. Sort keys must be all numbers or all texts. Ascending and stable."
|
||||
},
|
||||
starts_with: {
|
||||
signature: "starts_with(text, prefix)",
|
||||
description: "Returns true if the text starts with the given prefix."
|
||||
},
|
||||
stone: {
|
||||
signature: "stone(value)",
|
||||
description: "Petrify the value, making it permanently immutable. Deep freeze."
|
||||
},
|
||||
text: {
|
||||
signature: "text(value, ...)",
|
||||
description: "Convert to text. Polymorphic: text(array, sep) joins, text(number, radix/format) formats, text(text, from, to) substrings."
|
||||
},
|
||||
trim: {
|
||||
signature: "trim(text, reject)",
|
||||
description: "Remove characters from both ends. Default removes whitespace."
|
||||
},
|
||||
trunc: {
|
||||
signature: "trunc(number, place)",
|
||||
description: "Truncate toward zero."
|
||||
},
|
||||
upper: {
|
||||
signature: "upper(text)",
|
||||
description: "Returns text with all lowercase characters converted to uppercase."
|
||||
},
|
||||
whole: {
|
||||
signature: "whole(number)",
|
||||
description: "Returns the whole part of a number."
|
||||
},
|
||||
meme: {
|
||||
signature: "meme()",
|
||||
description: "Create a new meme (prototype chain marker)."
|
||||
},
|
||||
proto: {
|
||||
signature: "proto(object, meme)",
|
||||
description: "Set the prototype meme of an object."
|
||||
},
|
||||
isa: {
|
||||
signature: "isa(object, meme)",
|
||||
description: "Returns true if the object has the given meme in its prototype chain."
|
||||
},
|
||||
splat: {
|
||||
signature: "splat(function, array)",
|
||||
description: "Call function with array elements as separate arguments."
|
||||
},
|
||||
use: {
|
||||
signature: "use(path)",
|
||||
description: "Load a module. Returns the module's exported value. Modules are cached and frozen."
|
||||
},
|
||||
pi: {
|
||||
signature: "pi",
|
||||
description: "An approximation of circumference / diameter: 3.1415926535897932."
|
||||
}
|
||||
}
|
||||
|
||||
// Actor intrinsic documentation
|
||||
def actor_docs = {
|
||||
"$me": {
|
||||
signature: "$me",
|
||||
description: "The address of this actor."
|
||||
},
|
||||
"$send": {
|
||||
signature: "$send(address, message)",
|
||||
description: "Send a message to another actor."
|
||||
},
|
||||
"$start": {
|
||||
signature: "$start(script, env)",
|
||||
description: "Start a new actor from a script path."
|
||||
},
|
||||
"$stop": {
|
||||
signature: "$stop()",
|
||||
description: "Stop this actor."
|
||||
},
|
||||
"$delay": {
|
||||
signature: "$delay(milliseconds)",
|
||||
description: "Delay processing for a number of milliseconds."
|
||||
},
|
||||
"$receiver": {
|
||||
signature: "$receiver(function)",
|
||||
description: "Set the message receiver function for this actor."
|
||||
},
|
||||
"$clock": {
|
||||
signature: "$clock(interval, message)",
|
||||
description: "Send a message to self at regular intervals."
|
||||
},
|
||||
"$portal": {
|
||||
signature: "$portal(name)",
|
||||
description: "Create a named portal for inter-actor communication."
|
||||
},
|
||||
"$contact": {
|
||||
signature: "$contact(portal_name)",
|
||||
description: "Connect to a named portal."
|
||||
},
|
||||
"$couple": {
|
||||
signature: "$couple(address)",
|
||||
description: "Couple with another actor for lifecycle management."
|
||||
},
|
||||
"$unneeded": {
|
||||
signature: "$unneeded(function)",
|
||||
description: "Set a function to be called when this actor is no longer needed."
|
||||
},
|
||||
"$connection": {
|
||||
signature: "$connection(address)",
|
||||
description: "Establish a connection with another actor."
|
||||
},
|
||||
"$time_limit": {
|
||||
signature: "$time_limit(milliseconds)",
|
||||
description: "Set a time limit for this actor's execution."
|
||||
}
|
||||
}
|
||||
|
||||
// Provide hover info for a token.
|
||||
var hover = function(doc, line, col, token_at) {
|
||||
var tok = token_at(doc, line, col)
|
||||
var info = null
|
||||
var name = null
|
||||
var _i = 0
|
||||
var _j = 0
|
||||
var scope = null
|
||||
var v = null
|
||||
|
||||
if (tok == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Check intrinsic functions
|
||||
if (tok.kind == "name" && tok.value != null) {
|
||||
name = tok.value
|
||||
info = intrinsic_docs[name]
|
||||
if (info != null) {
|
||||
return {
|
||||
contents: {
|
||||
kind: "markdown",
|
||||
value: `**${info.signature}**\n\n${info.description}`
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check actor intrinsics ($name)
|
||||
if (tok.value != null && starts_with(tok.value, "$")) {
|
||||
info = actor_docs[tok.value]
|
||||
if (info != null) {
|
||||
return {
|
||||
contents: {
|
||||
kind: "markdown",
|
||||
value: `**${info.signature}**\n\n${info.description}`
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check keywords
|
||||
if (tok.kind == "var" || tok.kind == "def") {
|
||||
return {
|
||||
contents: {
|
||||
kind: "markdown",
|
||||
value: (tok.kind == "var")
|
||||
? "**var** — Declare a mutable variable."
|
||||
: "**def** — Declare a constant."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (tok.kind == "disrupt") {
|
||||
return {
|
||||
contents: {
|
||||
kind: "markdown",
|
||||
value: "**disrupt** — Raise an error. Use with **disruption** block to handle errors."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (tok.kind == "disruption") {
|
||||
return {
|
||||
contents: {
|
||||
kind: "markdown",
|
||||
value: "**disruption** — Error handling block. Catches errors raised by **disrupt**."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// User variable: show declaration info from scope
|
||||
if (tok.kind == "name" && tok.value != null && doc.ast != null && doc.ast.scopes != null) {
|
||||
_i = 0
|
||||
while (_i < length(doc.ast.scopes)) {
|
||||
scope = doc.ast.scopes[_i]
|
||||
if (scope.vars != null) {
|
||||
_j = 0
|
||||
while (_j < length(scope.vars)) {
|
||||
v = scope.vars[_j]
|
||||
if (v.name == tok.value) {
|
||||
return {
|
||||
contents: {
|
||||
kind: "markdown",
|
||||
value: (v.is_const == true)
|
||||
? `**def** ${v.name}`
|
||||
: `**var** ${v.name}`
|
||||
}
|
||||
}
|
||||
}
|
||||
_j = _j + 1
|
||||
}
|
||||
}
|
||||
_i = _i + 1
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
hover: hover,
|
||||
intrinsic_docs: intrinsic_docs,
|
||||
actor_docs: actor_docs
|
||||
}
|
||||
209
editors/vscode/lsp/lsp.ce
Normal file
209
editors/vscode/lsp/lsp.ce
Normal file
@@ -0,0 +1,209 @@
|
||||
// ƿit Language Server Protocol (LSP) main loop.
|
||||
// Communicates via JSON-RPC over stdin/stdout.
|
||||
|
||||
var fd = use('fd')
|
||||
var json_mod = use('json')
|
||||
var protocol = use('protocol')
|
||||
var analysis_make = use('analysis')
|
||||
var completions = use('completions')
|
||||
var hover_mod = use('hover')
|
||||
var symbols = use('symbols')
|
||||
|
||||
// Get tokenize_mod and parse_mod from the environment.
|
||||
// These are the same functions the compiler uses internally.
|
||||
var tokenize_mod = use('tokenize')
|
||||
var parse_mod = use('parse')
|
||||
|
||||
// Create analysis module bound to tokenize/parse
|
||||
var analysis = analysis_make(tokenize_mod, parse_mod)
|
||||
|
||||
// Document store: URI -> {text, version, ast, tokens, errors}
|
||||
var docs = {}
|
||||
|
||||
// Log to stderr for debugging (does not interfere with protocol).
|
||||
var log = function(msg) {
|
||||
fd.write(2, `[pit-lsp] ${msg}\n`)
|
||||
}
|
||||
|
||||
// Publish diagnostics for a document.
|
||||
var publish_diagnostics = function(uri, doc) {
|
||||
var diags = analysis.diagnostics(doc)
|
||||
protocol.notify("textDocument/publishDiagnostics", {
|
||||
uri: uri,
|
||||
diagnostics: diags
|
||||
})
|
||||
}
|
||||
|
||||
// Parse a document and publish diagnostics.
|
||||
var parse_and_notify = function(uri, src, version) {
|
||||
var doc = analysis.update(docs, uri, {src: src, version: version})
|
||||
publish_diagnostics(uri, doc)
|
||||
}
|
||||
|
||||
// Handle initialize request.
|
||||
var handle_initialize = function(id, params) {
|
||||
protocol.respond(id, {
|
||||
capabilities: {
|
||||
textDocumentSync: {
|
||||
openClose: true,
|
||||
change: 1,
|
||||
save: {includeText: true}
|
||||
},
|
||||
completionProvider: {
|
||||
triggerCharacters: [".", "$"]
|
||||
},
|
||||
hoverProvider: true,
|
||||
definitionProvider: true,
|
||||
documentSymbolProvider: true
|
||||
},
|
||||
serverInfo: {
|
||||
name: "pit-lsp",
|
||||
version: "0.1.0"
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Handle textDocument/didOpen notification.
|
||||
var handle_did_open = function(params) {
|
||||
var td = params.textDocument
|
||||
parse_and_notify(td.uri, td.text, td.version)
|
||||
}
|
||||
|
||||
// Handle textDocument/didChange notification (full text sync).
|
||||
var handle_did_change = function(params) {
|
||||
var td = params.textDocument
|
||||
var changes = params.contentChanges
|
||||
if (length(changes) > 0) {
|
||||
parse_and_notify(td.uri, changes[0].text, td.version)
|
||||
}
|
||||
}
|
||||
|
||||
// Handle textDocument/didClose notification.
|
||||
var handle_did_close = function(params) {
|
||||
var uri = params.textDocument.uri
|
||||
analysis.remove(docs, uri)
|
||||
// Clear diagnostics
|
||||
protocol.notify("textDocument/publishDiagnostics", {
|
||||
uri: uri,
|
||||
diagnostics: []
|
||||
})
|
||||
}
|
||||
|
||||
// Handle textDocument/didSave notification.
|
||||
var handle_did_save = function(params) {
|
||||
var td = params.textDocument
|
||||
if (params.text != null) {
|
||||
parse_and_notify(td.uri, params.text, td.version)
|
||||
}
|
||||
}
|
||||
|
||||
// Handle textDocument/completion request.
|
||||
var handle_completion = function(id, params) {
|
||||
var uri = params.textDocument.uri
|
||||
var pos = params.position
|
||||
var doc = docs[uri]
|
||||
var items = []
|
||||
if (doc != null) {
|
||||
items = completions.complete(doc, pos.line, pos.character)
|
||||
}
|
||||
protocol.respond(id, items)
|
||||
}
|
||||
|
||||
// Handle textDocument/hover request.
|
||||
var handle_hover = function(id, params) {
|
||||
var uri = params.textDocument.uri
|
||||
var pos = params.position
|
||||
var doc = docs[uri]
|
||||
var result = null
|
||||
if (doc != null) {
|
||||
result = hover_mod.hover(doc, pos.line, pos.character, analysis.token_at)
|
||||
}
|
||||
protocol.respond(id, result)
|
||||
}
|
||||
|
||||
// Handle textDocument/definition request.
|
||||
var handle_definition = function(id, params) {
|
||||
var uri = params.textDocument.uri
|
||||
var pos = params.position
|
||||
var doc = docs[uri]
|
||||
var result = null
|
||||
if (doc != null) {
|
||||
result = symbols.definition(doc, pos.line, pos.character, analysis.token_at)
|
||||
}
|
||||
protocol.respond(id, result)
|
||||
}
|
||||
|
||||
// Handle textDocument/documentSymbol request.
|
||||
var handle_document_symbol = function(id, params) {
|
||||
var uri = params.textDocument.uri
|
||||
var doc = docs[uri]
|
||||
var result = []
|
||||
if (doc != null) {
|
||||
result = symbols.document_symbols(doc)
|
||||
}
|
||||
protocol.respond(id, result)
|
||||
}
|
||||
|
||||
// Dispatch a single message. Wrapped in a function for disruption handling.
|
||||
var dispatch_message = function(msg) {
|
||||
var method = msg.method
|
||||
if (method == "initialize") {
|
||||
handle_initialize(msg.id, msg.params)
|
||||
} else if (method == "initialized") {
|
||||
// no-op
|
||||
} else if (method == "textDocument/didOpen") {
|
||||
handle_did_open(msg.params)
|
||||
} else if (method == "textDocument/didChange") {
|
||||
handle_did_change(msg.params)
|
||||
} else if (method == "textDocument/didClose") {
|
||||
handle_did_close(msg.params)
|
||||
} else if (method == "textDocument/didSave") {
|
||||
handle_did_save(msg.params)
|
||||
} else if (method == "textDocument/completion") {
|
||||
handle_completion(msg.id, msg.params)
|
||||
} else if (method == "textDocument/hover") {
|
||||
handle_hover(msg.id, msg.params)
|
||||
} else if (method == "textDocument/definition") {
|
||||
handle_definition(msg.id, msg.params)
|
||||
} else if (method == "textDocument/documentSymbol") {
|
||||
handle_document_symbol(msg.id, msg.params)
|
||||
} else if (method == "shutdown") {
|
||||
protocol.respond(msg.id, null)
|
||||
return "shutdown"
|
||||
} else if (method == "exit") {
|
||||
return "exit"
|
||||
} else {
|
||||
if (msg.id != null) {
|
||||
protocol.respond_error(msg.id, -32601, `Method not found: ${method}`)
|
||||
}
|
||||
}
|
||||
return null
|
||||
} disruption {
|
||||
log(`error handling ${msg.method}`)
|
||||
if (msg.id != null) {
|
||||
protocol.respond_error(msg.id, -32603, `Internal error handling ${msg.method}`)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// Main loop.
|
||||
log("starting")
|
||||
|
||||
var running = true
|
||||
var msg = null
|
||||
var result = null
|
||||
|
||||
while (running) {
|
||||
msg = protocol.read_message()
|
||||
if (msg == null) {
|
||||
running = false
|
||||
break
|
||||
}
|
||||
|
||||
result = dispatch_message(msg)
|
||||
if (result == "exit") {
|
||||
running = false
|
||||
}
|
||||
}
|
||||
|
||||
log("stopped")
|
||||
102
editors/vscode/lsp/protocol.cm
Normal file
102
editors/vscode/lsp/protocol.cm
Normal file
@@ -0,0 +1,102 @@
|
||||
// JSON-RPC protocol helpers for LSP communication over stdin/stdout.
|
||||
// Reads Content-Length framed messages from stdin, writes to stdout.
|
||||
|
||||
var fd = use('fd')
|
||||
var json = use('json')
|
||||
|
||||
// Read a single JSON-RPC message from stdin.
|
||||
// Protocol: "Content-Length: N\r\n\r\n" followed by N bytes of JSON.
|
||||
var read_message = function() {
|
||||
var header = ""
|
||||
var ch = null
|
||||
var content_length = null
|
||||
var body = null
|
||||
var total = 0
|
||||
var chunk = null
|
||||
|
||||
// Read header byte by byte until we hit \r\n\r\n
|
||||
while (true) {
|
||||
ch = fd.read(0, 1)
|
||||
if (ch == null) {
|
||||
return null
|
||||
}
|
||||
header = header + text(ch)
|
||||
if (ends_with(header, "\r\n\r\n")) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Parse Content-Length from header
|
||||
var lines = array(header, "\r\n")
|
||||
var _i = 0
|
||||
while (_i < length(lines)) {
|
||||
if (starts_with(lines[_i], "Content-Length:")) {
|
||||
content_length = number(trim(text(lines[_i], 16)))
|
||||
}
|
||||
_i = _i + 1
|
||||
}
|
||||
|
||||
if (content_length == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Read exactly content_length bytes
|
||||
body = ""
|
||||
total = 0
|
||||
while (total < content_length) {
|
||||
chunk = fd.read(0, content_length - total)
|
||||
if (chunk == null) {
|
||||
return null
|
||||
}
|
||||
chunk = text(chunk)
|
||||
body = body + chunk
|
||||
total = total + length(chunk)
|
||||
}
|
||||
|
||||
return json.decode(body)
|
||||
}
|
||||
|
||||
// Send a JSON-RPC message to stdout.
|
||||
var send_message = function(msg) {
|
||||
var body = json.encode(msg)
|
||||
var header = `Content-Length: ${text(length(body))}\r\n\r\n`
|
||||
fd.write(1, header + body)
|
||||
}
|
||||
|
||||
// Send a JSON-RPC response for a request.
|
||||
var respond = function(id, result) {
|
||||
send_message({
|
||||
jsonrpc: "2.0",
|
||||
id: id,
|
||||
result: result
|
||||
})
|
||||
}
|
||||
|
||||
// Send a JSON-RPC error response.
|
||||
var respond_error = function(id, code, message) {
|
||||
send_message({
|
||||
jsonrpc: "2.0",
|
||||
id: id,
|
||||
error: {
|
||||
code: code,
|
||||
message: message
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Send a JSON-RPC notification (no id).
|
||||
var notify = function(method, params) {
|
||||
send_message({
|
||||
jsonrpc: "2.0",
|
||||
method: method,
|
||||
params: params
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
read_message: read_message,
|
||||
send_message: send_message,
|
||||
respond: respond,
|
||||
respond_error: respond_error,
|
||||
notify: notify
|
||||
}
|
||||
238
editors/vscode/lsp/symbols.cm
Normal file
238
editors/vscode/lsp/symbols.cm
Normal file
@@ -0,0 +1,238 @@
|
||||
// Document symbols and go-to-definition provider for the ƿit LSP.
|
||||
|
||||
// SymbolKind constants (LSP spec)
|
||||
def KIND_FUNCTION = 12
|
||||
def KIND_VARIABLE = 13
|
||||
def KIND_CONSTANT = 14
|
||||
|
||||
// Walk AST to extract document symbols (top-level vars/defs and functions).
|
||||
var document_symbols = function(doc) {
|
||||
var symbols = []
|
||||
var ast = doc.ast
|
||||
var _i = 0
|
||||
var _j = 0
|
||||
var stmt = null
|
||||
var decl = null
|
||||
var name = null
|
||||
var kind = null
|
||||
var range = null
|
||||
|
||||
if (ast == null || ast.statements == null) {
|
||||
return symbols
|
||||
}
|
||||
|
||||
while (_i < length(ast.statements)) {
|
||||
stmt = ast.statements[_i]
|
||||
|
||||
if (stmt.kind == "var" || stmt.kind == "def") {
|
||||
name = null
|
||||
kind = KIND_VARIABLE
|
||||
|
||||
if (stmt.left != null && stmt.left.name != null) {
|
||||
name = stmt.left.name
|
||||
}
|
||||
|
||||
if (stmt.kind == "def") {
|
||||
kind = KIND_CONSTANT
|
||||
}
|
||||
|
||||
if (stmt.right != null && (stmt.right.kind == "function" || stmt.right.kind == "arrow function")) {
|
||||
kind = KIND_FUNCTION
|
||||
}
|
||||
|
||||
if (name != null) {
|
||||
range = {
|
||||
start: {line: stmt.from_row, character: stmt.from_column},
|
||||
end: {line: stmt.to_row, character: stmt.to_column}
|
||||
}
|
||||
symbols[] = {
|
||||
name: name,
|
||||
kind: kind,
|
||||
range: range,
|
||||
selectionRange: {
|
||||
start: {line: stmt.left.from_row, character: stmt.left.from_column},
|
||||
end: {line: stmt.left.to_row, character: stmt.left.to_column}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (stmt.kind == "var_list" && stmt.list != null) {
|
||||
_j = 0
|
||||
while (_j < length(stmt.list)) {
|
||||
decl = stmt.list[_j]
|
||||
if (decl.left != null && decl.left.name != null) {
|
||||
kind = (decl.kind == "def") ? KIND_CONSTANT : KIND_VARIABLE
|
||||
if (decl.right != null && (decl.right.kind == "function" || decl.right.kind == "arrow function")) {
|
||||
kind = KIND_FUNCTION
|
||||
}
|
||||
range = {
|
||||
start: {line: decl.from_row, character: decl.from_column},
|
||||
end: {line: decl.to_row, character: decl.to_column}
|
||||
}
|
||||
symbols[] = {
|
||||
name: decl.left.name,
|
||||
kind: kind,
|
||||
range: range,
|
||||
selectionRange: {
|
||||
start: {line: decl.left.from_row, character: decl.left.from_column},
|
||||
end: {line: decl.left.to_row, character: decl.left.to_column}
|
||||
}
|
||||
}
|
||||
}
|
||||
_j = _j + 1
|
||||
}
|
||||
}
|
||||
|
||||
_i = _i + 1
|
||||
}
|
||||
|
||||
return symbols
|
||||
}
|
||||
|
||||
// Find the declaration location of a name at a given position.
|
||||
var definition = function(doc, line, col, token_at) {
|
||||
var tok = token_at(doc, line, col)
|
||||
var ast = doc.ast
|
||||
var name = null
|
||||
var _i = 0
|
||||
var _j = 0
|
||||
var scope = null
|
||||
var v = null
|
||||
var decl = null
|
||||
|
||||
if (tok == null || tok.kind != "name" || tok.value == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
name = tok.value
|
||||
|
||||
if (ast == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Search through scopes for the variable declaration
|
||||
if (ast.scopes != null) {
|
||||
_i = 0
|
||||
while (_i < length(ast.scopes)) {
|
||||
scope = ast.scopes[_i]
|
||||
if (scope.vars != null) {
|
||||
_j = 0
|
||||
while (_j < length(scope.vars)) {
|
||||
v = scope.vars[_j]
|
||||
if (v.name == name) {
|
||||
decl = find_declaration(ast.statements, name)
|
||||
if (decl != null) {
|
||||
return {
|
||||
uri: doc.uri,
|
||||
range: {
|
||||
start: {line: decl.from_row, character: decl.from_column},
|
||||
end: {line: decl.to_row, character: decl.to_column}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_j = _j + 1
|
||||
}
|
||||
}
|
||||
_i = _i + 1
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: walk statements for var/def with this name
|
||||
decl = find_declaration(ast.statements, name)
|
||||
if (decl != null) {
|
||||
return {
|
||||
uri: doc.uri,
|
||||
range: {
|
||||
start: {line: decl.from_row, character: decl.from_column},
|
||||
end: {line: decl.to_row, character: decl.to_column}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
// Recursively search statements for a var/def declaration of a given name.
|
||||
var find_declaration = function(statements, name) {
|
||||
var _i = 0
|
||||
var _j = 0
|
||||
var stmt = null
|
||||
var result = null
|
||||
|
||||
if (statements == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
while (_i < length(statements)) {
|
||||
stmt = statements[_i]
|
||||
|
||||
// Direct var/def
|
||||
if ((stmt.kind == "var" || stmt.kind == "def")
|
||||
&& stmt.left != null && stmt.left.name == name) {
|
||||
return stmt
|
||||
}
|
||||
|
||||
// var_list
|
||||
if (stmt.kind == "var_list" && stmt.list != null) {
|
||||
_j = 0
|
||||
while (_j < length(stmt.list)) {
|
||||
if (stmt.list[_j].left != null && stmt.list[_j].left.name == name) {
|
||||
return stmt.list[_j]
|
||||
}
|
||||
_j = _j + 1
|
||||
}
|
||||
}
|
||||
|
||||
// Recurse into blocks
|
||||
if (stmt.statements != null) {
|
||||
result = find_declaration(stmt.statements, name)
|
||||
if (result != null) {
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
// if/else
|
||||
if (stmt.kind == "if") {
|
||||
if (stmt.then != null && stmt.then.statements != null) {
|
||||
result = find_declaration(stmt.then.statements, name)
|
||||
if (result != null) {
|
||||
return result
|
||||
}
|
||||
}
|
||||
if (stmt.else != null && stmt.else.statements != null) {
|
||||
result = find_declaration(stmt.else.statements, name)
|
||||
if (result != null) {
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Function body
|
||||
if ((stmt.kind == "function" || stmt.kind == "arrow function") && stmt.statements != null) {
|
||||
result = find_declaration(stmt.statements, name)
|
||||
if (result != null) {
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
// var/def with function right side
|
||||
if ((stmt.kind == "var" || stmt.kind == "def") && stmt.right != null) {
|
||||
if ((stmt.right.kind == "function" || stmt.right.kind == "arrow function") && stmt.right.statements != null) {
|
||||
result = find_declaration(stmt.right.statements, name)
|
||||
if (result != null) {
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_i = _i + 1
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
document_symbols: document_symbols,
|
||||
definition: definition
|
||||
}
|
||||
62
editors/vscode/package.json
Normal file
62
editors/vscode/package.json
Normal file
@@ -0,0 +1,62 @@
|
||||
{
|
||||
"name": "pit-language",
|
||||
"displayName": "ƿit Language",
|
||||
"description": "Language support for ƿit (.ce/.cm) — syntax highlighting, diagnostics, completions, hover, and go-to-definition",
|
||||
"version": "0.1.0",
|
||||
"publisher": "pit-lang",
|
||||
"engines": {
|
||||
"vscode": "^1.75.0"
|
||||
},
|
||||
"categories": [
|
||||
"Programming Languages"
|
||||
],
|
||||
"activationEvents": [
|
||||
"onLanguage:pit"
|
||||
],
|
||||
"main": "./out/extension.js",
|
||||
"contributes": {
|
||||
"languages": [
|
||||
{
|
||||
"id": "pit",
|
||||
"aliases": [
|
||||
"ƿit",
|
||||
"pit"
|
||||
],
|
||||
"extensions": [
|
||||
".ce",
|
||||
".cm"
|
||||
],
|
||||
"configuration": "./language-configuration.json"
|
||||
}
|
||||
],
|
||||
"grammars": [
|
||||
{
|
||||
"language": "pit",
|
||||
"scopeName": "source.pit",
|
||||
"path": "./syntaxes/pit.tmLanguage.json"
|
||||
}
|
||||
],
|
||||
"configuration": {
|
||||
"title": "ƿit",
|
||||
"properties": {
|
||||
"pit.cellPath": {
|
||||
"type": "string",
|
||||
"default": "cell",
|
||||
"description": "Path to the cell executable"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"compile": "tsc -p ./",
|
||||
"watch": "tsc -watch -p ./"
|
||||
},
|
||||
"dependencies": {
|
||||
"vscode-languageclient": "^9.0.0",
|
||||
"vscode-languageserver-protocol": "^3.17.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/vscode": "^1.75.0",
|
||||
"typescript": "^5.0.0"
|
||||
}
|
||||
}
|
||||
44
editors/vscode/src/extension.ts
Normal file
44
editors/vscode/src/extension.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import * as path from "path";
|
||||
import { workspace, ExtensionContext } from "vscode";
|
||||
import {
|
||||
LanguageClient,
|
||||
LanguageClientOptions,
|
||||
ServerOptions,
|
||||
} from "vscode-languageclient/node";
|
||||
|
||||
let client: LanguageClient;
|
||||
|
||||
export function activate(context: ExtensionContext) {
|
||||
const config = workspace.getConfiguration("pit");
|
||||
const cellPath = config.get<string>("cellPath", "cell");
|
||||
const lspDir = path.join(context.extensionPath, "lsp");
|
||||
|
||||
const serverOptions: ServerOptions = {
|
||||
command: cellPath,
|
||||
args: ["lsp/lsp"],
|
||||
options: { cwd: lspDir },
|
||||
};
|
||||
|
||||
const clientOptions: LanguageClientOptions = {
|
||||
documentSelector: [{ scheme: "file", language: "pit" }],
|
||||
synchronize: {
|
||||
fileEvents: workspace.createFileSystemWatcher("**/*.{ce,cm}"),
|
||||
},
|
||||
};
|
||||
|
||||
client = new LanguageClient(
|
||||
"pitLanguageServer",
|
||||
"ƿit Language Server",
|
||||
serverOptions,
|
||||
clientOptions
|
||||
);
|
||||
|
||||
client.start();
|
||||
}
|
||||
|
||||
export function deactivate(): Thenable<void> | undefined {
|
||||
if (!client) {
|
||||
return undefined;
|
||||
}
|
||||
return client.stop();
|
||||
}
|
||||
160
editors/vscode/syntaxes/pit.tmLanguage.json
Normal file
160
editors/vscode/syntaxes/pit.tmLanguage.json
Normal file
@@ -0,0 +1,160 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json",
|
||||
"name": "pit",
|
||||
"scopeName": "source.pit",
|
||||
"patterns": [
|
||||
{ "include": "#comment-line" },
|
||||
{ "include": "#comment-block" },
|
||||
{ "include": "#string-template" },
|
||||
{ "include": "#string-double" },
|
||||
{ "include": "#regexp" },
|
||||
{ "include": "#keyword-control" },
|
||||
{ "include": "#keyword-error" },
|
||||
{ "include": "#storage-type" },
|
||||
{ "include": "#constant-language" },
|
||||
{ "include": "#variable-language" },
|
||||
{ "include": "#actor-intrinsic" },
|
||||
{ "include": "#keyword-operator" },
|
||||
{ "include": "#arrow-function" },
|
||||
{ "include": "#support-function" },
|
||||
{ "include": "#constant-numeric-hex" },
|
||||
{ "include": "#constant-numeric-binary" },
|
||||
{ "include": "#constant-numeric-octal" },
|
||||
{ "include": "#constant-numeric" },
|
||||
{ "include": "#punctuation" }
|
||||
],
|
||||
"repository": {
|
||||
"comment-line": {
|
||||
"name": "comment.line.double-slash.pit",
|
||||
"match": "//.*$"
|
||||
},
|
||||
"comment-block": {
|
||||
"name": "comment.block.pit",
|
||||
"begin": "/\\*",
|
||||
"end": "\\*/",
|
||||
"beginCaptures": { "0": { "name": "punctuation.definition.comment.begin.pit" } },
|
||||
"endCaptures": { "0": { "name": "punctuation.definition.comment.end.pit" } }
|
||||
},
|
||||
"string-double": {
|
||||
"name": "string.quoted.double.pit",
|
||||
"begin": "\"",
|
||||
"end": "\"",
|
||||
"beginCaptures": { "0": { "name": "punctuation.definition.string.begin.pit" } },
|
||||
"endCaptures": { "0": { "name": "punctuation.definition.string.end.pit" } },
|
||||
"patterns": [
|
||||
{
|
||||
"name": "constant.character.escape.pit",
|
||||
"match": "\\\\(?:[\"\\\\bfnrt/]|u[0-9a-fA-F]{4})"
|
||||
}
|
||||
]
|
||||
},
|
||||
"string-template": {
|
||||
"name": "string.template.pit",
|
||||
"begin": "`",
|
||||
"end": "`",
|
||||
"beginCaptures": { "0": { "name": "punctuation.definition.string.template.begin.pit" } },
|
||||
"endCaptures": { "0": { "name": "punctuation.definition.string.template.end.pit" } },
|
||||
"patterns": [
|
||||
{
|
||||
"name": "constant.character.escape.pit",
|
||||
"match": "\\\\(?:[`\\\\bfnrt/$]|u[0-9a-fA-F]{4})"
|
||||
},
|
||||
{
|
||||
"name": "meta.template.expression.pit",
|
||||
"begin": "\\$\\{",
|
||||
"end": "\\}",
|
||||
"beginCaptures": { "0": { "name": "punctuation.definition.template-expression.begin.pit" } },
|
||||
"endCaptures": { "0": { "name": "punctuation.definition.template-expression.end.pit" } },
|
||||
"patterns": [
|
||||
{ "include": "source.pit" }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"regexp": {
|
||||
"name": "string.regexp.pit",
|
||||
"begin": "(?<=[=(:,;!&|?~^>]|^|return|disrupt)\\s*(/(?![/*]))",
|
||||
"end": "/([gimsuvy]*)",
|
||||
"beginCaptures": { "1": { "name": "punctuation.definition.string.begin.pit" } },
|
||||
"endCaptures": { "1": { "name": "keyword.other.pit" } },
|
||||
"patterns": [
|
||||
{
|
||||
"name": "constant.character.escape.pit",
|
||||
"match": "\\\\."
|
||||
}
|
||||
]
|
||||
},
|
||||
"keyword-control": {
|
||||
"name": "keyword.control.pit",
|
||||
"match": "\\b(if|else|for|while|do|break|continue|return|go)\\b"
|
||||
},
|
||||
"keyword-error": {
|
||||
"name": "keyword.control.error.pit",
|
||||
"match": "\\b(disrupt|disruption)\\b"
|
||||
},
|
||||
"storage-type": {
|
||||
"patterns": [
|
||||
{
|
||||
"name": "storage.type.pit",
|
||||
"match": "\\b(var|def)\\b"
|
||||
},
|
||||
{
|
||||
"name": "storage.type.function.pit",
|
||||
"match": "\\bfunction\\b"
|
||||
}
|
||||
]
|
||||
},
|
||||
"constant-language": {
|
||||
"name": "constant.language.pit",
|
||||
"match": "\\b(null|true|false)\\b"
|
||||
},
|
||||
"variable-language": {
|
||||
"name": "variable.language.this.pit",
|
||||
"match": "\\bthis\\b"
|
||||
},
|
||||
"actor-intrinsic": {
|
||||
"name": "variable.language.actor.pit",
|
||||
"match": "\\$[a-zA-Z_][a-zA-Z0-9_]*"
|
||||
},
|
||||
"keyword-operator": {
|
||||
"name": "keyword.operator.pit",
|
||||
"match": "\\b(delete|in|typeof)\\b"
|
||||
},
|
||||
"arrow-function": {
|
||||
"name": "storage.type.function.arrow.pit",
|
||||
"match": "=>"
|
||||
},
|
||||
"support-function": {
|
||||
"name": "support.function.pit",
|
||||
"match": "\\b(abs|apply|array|ceiling|character|codepoint|ends_with|every|extract|fallback|filter|find|floor|for|format|fraction|is_array|is_blob|is_character|is_data|is_digit|is_false|is_fit|is_function|is_integer|is_letter|is_logical|is_lower|is_null|is_number|is_object|is_pattern|is_stone|is_text|is_true|is_upper|is_whitespace|length|logical|lower|max|min|modulo|neg|normalize|not|number|parallel|print|race|record|reduce|remainder|replace|reverse|round|search|sequence|sign|some|sort|starts_with|stone|text|trim|trunc|upper|whole|meme|proto|isa|splat|use)(?=\\s*\\()"
|
||||
},
|
||||
"constant-numeric-hex": {
|
||||
"name": "constant.numeric.hex.pit",
|
||||
"match": "\\b0[xX][0-9a-fA-F]+\\b"
|
||||
},
|
||||
"constant-numeric-binary": {
|
||||
"name": "constant.numeric.binary.pit",
|
||||
"match": "\\b0[bB][01]+\\b"
|
||||
},
|
||||
"constant-numeric-octal": {
|
||||
"name": "constant.numeric.octal.pit",
|
||||
"match": "\\b0[oO][0-7]+\\b"
|
||||
},
|
||||
"constant-numeric": {
|
||||
"name": "constant.numeric.pit",
|
||||
"match": "\\b[0-9]+(\\.[0-9]+)?([eE][+-]?[0-9]+)?\\b"
|
||||
},
|
||||
"punctuation": {
|
||||
"patterns": [
|
||||
{
|
||||
"name": "punctuation.separator.comma.pit",
|
||||
"match": ","
|
||||
},
|
||||
{
|
||||
"name": "punctuation.terminator.statement.pit",
|
||||
"match": ";"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
13
editors/vscode/tsconfig.json
Normal file
13
editors/vscode/tsconfig.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"target": "ES2020",
|
||||
"outDir": "out",
|
||||
"lib": ["ES2020"],
|
||||
"sourceMap": true,
|
||||
"rootDir": "src",
|
||||
"strict": true
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules", "out"]
|
||||
}
|
||||
@@ -1,234 +0,0 @@
|
||||
// HTTP Download Actor
|
||||
// Handles download requests and progress queries
|
||||
var http = use('http');
|
||||
var os = use('os');
|
||||
|
||||
// Actor state
|
||||
var state = {
|
||||
downloading: false,
|
||||
current_url: null,
|
||||
total_bytes: 0,
|
||||
downloaded_bytes: 0,
|
||||
start_time: 0,
|
||||
error: null,
|
||||
connection: null,
|
||||
download_msg: null,
|
||||
chunks: []
|
||||
};
|
||||
|
||||
// Helper to calculate progress percentage
|
||||
function get_progress() {
|
||||
if (state.total_bytes == 0) {
|
||||
return 0;
|
||||
}
|
||||
return number.round((state.downloaded_bytes / state.total_bytes) * 100);
|
||||
}
|
||||
|
||||
// Helper to format status response
|
||||
function get_status() {
|
||||
if (!state.downloading) {
|
||||
return {
|
||||
status: 'idle',
|
||||
error: state.error
|
||||
};
|
||||
}
|
||||
|
||||
var elapsed = os.now() - state.start_time;
|
||||
var bytes_per_sec = elapsed > 0 ? state.downloaded_bytes / elapsed : 0;
|
||||
|
||||
return {
|
||||
status: 'downloading',
|
||||
url: state.current_url,
|
||||
progress: get_progress(),
|
||||
downloaded_bytes: state.downloaded_bytes,
|
||||
total_bytes: state.total_bytes,
|
||||
elapsed_seconds: elapsed,
|
||||
bytes_per_second: number.round(bytes_per_sec)
|
||||
};
|
||||
}
|
||||
|
||||
// Main message receiver
|
||||
$receiver(function(msg) {
|
||||
switch (msg.type) {
|
||||
case 'download':
|
||||
if (state.downloading) {
|
||||
send(msg, {
|
||||
type: 'error',
|
||||
error: 'Already downloading',
|
||||
current_url: state.current_url
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!msg.url) {
|
||||
send(msg, {
|
||||
type: 'error',
|
||||
error: 'No URL provided'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Start download
|
||||
state.downloading = true;
|
||||
state.current_url = msg.url;
|
||||
state.total_bytes = 0;
|
||||
state.downloaded_bytes = 0;
|
||||
state.start_time = os.now();
|
||||
state.error = null;
|
||||
state.download_msg = msg;
|
||||
state.chunks = [];
|
||||
|
||||
try {
|
||||
// Start the connection
|
||||
state.connection = http.fetch_start(msg.url, msg.options || {});
|
||||
if (!state.connection) {
|
||||
throw new Error('Failed to start download');
|
||||
}
|
||||
|
||||
// Schedule the first chunk read
|
||||
$delay(read_next_chunk, 0);
|
||||
|
||||
} catch (e) {
|
||||
state.error = e.toString();
|
||||
state.downloading = false;
|
||||
|
||||
send(msg, {
|
||||
type: 'error',
|
||||
error: state.error,
|
||||
url: msg.url
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
case 'status':
|
||||
log.console(`got status request. current is ${get_status()}`)
|
||||
send(msg, {
|
||||
type: 'status_response',
|
||||
...get_status()
|
||||
});
|
||||
break;
|
||||
|
||||
case 'cancel':
|
||||
if (state.downloading) {
|
||||
// Cancel the download
|
||||
if (state.connection) {
|
||||
http.fetch_close(state.connection);
|
||||
state.connection = null;
|
||||
}
|
||||
state.downloading = false;
|
||||
state.current_url = null;
|
||||
state.download_msg = null;
|
||||
state.chunks = [];
|
||||
|
||||
send(msg, {
|
||||
type: 'cancelled',
|
||||
message: 'Download cancelled',
|
||||
url: state.current_url
|
||||
});
|
||||
} else {
|
||||
send(msg, {
|
||||
type: 'error',
|
||||
error: 'No download in progress'
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
send(msg, {
|
||||
type: 'error',
|
||||
error: 'Unknown message type: ' + msg.type
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Non-blocking chunk reader
|
||||
function read_next_chunk() {
|
||||
if (!state.downloading || !state.connection) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
var chunk = http.fetch_read_chunk(state.connection);
|
||||
|
||||
if (chunk == null) {
|
||||
// Download complete
|
||||
finish_download();
|
||||
return;
|
||||
}
|
||||
|
||||
// Store chunk
|
||||
state.chunks.push(chunk);
|
||||
|
||||
// Update progress
|
||||
var info = http.fetch_info(state.connection);
|
||||
state.downloaded_bytes = info.bytes_read;
|
||||
if (info.headers_complete && info.content_length > 0) {
|
||||
state.total_bytes = info.content_length;
|
||||
}
|
||||
|
||||
// Schedule next chunk read
|
||||
$delay(read_next_chunk, 0);
|
||||
|
||||
} catch (e) {
|
||||
// Error during download
|
||||
state.error = e.toString();
|
||||
if (state.connection) {
|
||||
http.fetch_close(state.connection);
|
||||
}
|
||||
|
||||
if (state.download_msg) {
|
||||
send(state.download_msg, {
|
||||
type: 'error',
|
||||
error: state.error,
|
||||
url: state.current_url
|
||||
});
|
||||
}
|
||||
|
||||
// Reset state
|
||||
state.downloading = false;
|
||||
state.connection = null;
|
||||
state.download_msg = null;
|
||||
state.chunks = [];
|
||||
}
|
||||
}
|
||||
|
||||
// Complete the download and send result
|
||||
function finish_download() {
|
||||
if (state.connection) {
|
||||
http.fetch_close(state.connection);
|
||||
}
|
||||
|
||||
// Combine all chunks into single ArrayBuffer
|
||||
var total_size = 0;
|
||||
for (var i = 0; i < state.chunks.length; i++) {
|
||||
total_size += state.chunks[i].byteLength;
|
||||
}
|
||||
|
||||
var result = new ArrayBuffer(total_size);
|
||||
var view = new Uint8Array(result);
|
||||
var offset = 0;
|
||||
|
||||
for (var i = 0; i < state.chunks.length; i++) {
|
||||
var chunk_view = new Uint8Array(state.chunks[i]);
|
||||
view.set(chunk_view, offset);
|
||||
offset += state.chunks[i].byteLength;
|
||||
}
|
||||
|
||||
// Send complete message
|
||||
if (state.download_msg) {
|
||||
send(state.download_msg, {
|
||||
type: 'complete',
|
||||
url: state.current_url,
|
||||
data: result,
|
||||
size: result.byteLength,
|
||||
duration: os.now() - state.start_time
|
||||
});
|
||||
}
|
||||
|
||||
// Reset state
|
||||
state.downloading = false;
|
||||
state.connection = null;
|
||||
state.current_url = null;
|
||||
state.download_msg = null;
|
||||
state.chunks = [];
|
||||
}
|
||||
@@ -10,7 +10,7 @@ var match_id = 0;
|
||||
$portal(e => {
|
||||
log.console("NAT server: received connection request");
|
||||
|
||||
if (!isa(e.actor, actor))
|
||||
if (!is_actor(e.actor))
|
||||
send(e, {reason: "Must provide the actor you want to connect."});
|
||||
|
||||
if (waiting_client) {
|
||||
|
||||
93
fash.c
Normal file
93
fash.c
Normal file
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
Fash64: Douglas Crockford (2017-02-02)
|
||||
64-bit hash that uses the high 64 bits of a 128-bit product for feedback.
|
||||
|
||||
Notes:
|
||||
- Requires a way to get the high half of a 64x64->128 multiply.
|
||||
- Uses __uint128_t when available; otherwise uses MSVC _umul128.
|
||||
*/
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
|
||||
typedef struct fash64_state {
|
||||
uint64_t result;
|
||||
uint64_t sum;
|
||||
} fash64_state;
|
||||
|
||||
enum {
|
||||
FASH64_PRIME_11 = 11111111111111111027ull,
|
||||
FASH64_PRIME_8 = 8888888888888888881ull,
|
||||
FASH64_PRIME_3 = 3333333333333333271ull
|
||||
};
|
||||
|
||||
static inline void fash64_mul_hi_lo(uint64_t a, uint64_t b, uint64_t *hi, uint64_t *lo)
|
||||
{
|
||||
#if defined(__SIZEOF_INT128__)
|
||||
__uint128_t p = (__uint128_t)a * (__uint128_t)b;
|
||||
*lo = (uint64_t)p;
|
||||
*hi = (uint64_t)(p >> 64);
|
||||
#elif defined(_MSC_VER) && defined(_M_X64)
|
||||
*lo = _umul128(a, b, hi);
|
||||
#else
|
||||
/* Portable fallback (no 128-bit type, no _umul128). */
|
||||
uint64_t a0 = (uint32_t)a;
|
||||
uint64_t a1 = a >> 32;
|
||||
uint64_t b0 = (uint32_t)b;
|
||||
uint64_t b1 = b >> 32;
|
||||
|
||||
uint64_t p00 = a0 * b0;
|
||||
uint64_t p01 = a0 * b1;
|
||||
uint64_t p10 = a1 * b0;
|
||||
uint64_t p11 = a1 * b1;
|
||||
|
||||
uint64_t mid = (p00 >> 32) + (uint32_t)p01 + (uint32_t)p10;
|
||||
*lo = (p00 & 0xffffffffull) | (mid << 32);
|
||||
*hi = p11 + (p01 >> 32) + (p10 >> 32) + (mid >> 32);
|
||||
#endif
|
||||
}
|
||||
|
||||
static inline void fash64_begin(fash64_state *s)
|
||||
{
|
||||
s->result = (uint64_t)FASH64_PRIME_8;
|
||||
s->sum = (uint64_t)FASH64_PRIME_3;
|
||||
}
|
||||
|
||||
static inline void fash64_word(fash64_state *s, uint64_t word)
|
||||
{
|
||||
uint64_t high, low;
|
||||
uint64_t mixed = s->result ^ word;
|
||||
|
||||
fash64_mul_hi_lo(mixed, (uint64_t)FASH64_PRIME_11, &high, &low);
|
||||
|
||||
s->sum += high;
|
||||
s->result = low ^ s->sum;
|
||||
}
|
||||
|
||||
static inline void fash64_block(fash64_state *s, const uint64_t *block, size_t word_count)
|
||||
{
|
||||
for (size_t i = 0; i < word_count; i++) fash64_word(s, block[i]);
|
||||
}
|
||||
|
||||
static inline uint64_t fash64_end(const fash64_state *s)
|
||||
{
|
||||
return s->result;
|
||||
}
|
||||
|
||||
/* Convenience one-shot helper */
|
||||
static inline uint64_t fash64_hash_words(const uint64_t *words, size_t word_count, uint64_t extra_word)
|
||||
{
|
||||
fash64_state s;
|
||||
fash64_begin(&s);
|
||||
fash64_block(&s, words, word_count);
|
||||
fash64_word(&s, extra_word);
|
||||
return fash64_end(&s);
|
||||
}
|
||||
|
||||
static inline uint64_t fash64_hash_one(uint64_t word)
|
||||
{
|
||||
uint64_t high, low;
|
||||
uint64_t mixed = (uint64_t)FASH64_PRIME_8 ^ word;
|
||||
fash64_mul_hi_lo(mixed, (uint64_t)FASH64_PRIME_11, &high, &low);
|
||||
return low ^ ((uint64_t)FASH64_PRIME_3 + high);
|
||||
}
|
||||
23
fd.c
23
fd.c
@@ -50,7 +50,7 @@ JSC_SCALL(fd_open,
|
||||
mode_t mode = 0644;
|
||||
|
||||
// Parse optional flags argument
|
||||
if (argc > 1 && JS_IsString(argv[1])) {
|
||||
if (argc > 1 && JS_IsText(argv[1])) {
|
||||
const char *flag_str = JS_ToCString(js, argv[1]);
|
||||
flags = 0;
|
||||
|
||||
@@ -78,7 +78,7 @@ JSC_CCALL(fd_write,
|
||||
|
||||
size_t len;
|
||||
ssize_t wrote;
|
||||
if (JS_IsString(argv[1])) {
|
||||
if (JS_IsText(argv[1])) {
|
||||
const char *data = JS_ToCStringLen(js, &len, argv[1]);
|
||||
if (!data) return JS_EXCEPTION;
|
||||
wrote = write(fd, data, len);
|
||||
@@ -276,7 +276,7 @@ JSC_SCALL(fd_mkdir,
|
||||
JSC_SCALL(fd_mv,
|
||||
if (argc < 2)
|
||||
ret = JS_ThrowTypeError(js, "fd.mv requires 2 arguments: old path and new path");
|
||||
else if (!JS_IsString(argv[1]))
|
||||
else if (!JS_IsText(argv[1]))
|
||||
ret = JS_ThrowTypeError(js, "second argument must be a string (new path)");
|
||||
else {
|
||||
const char *new_path = JS_ToCString(js, argv[1]);
|
||||
@@ -289,7 +289,7 @@ JSC_SCALL(fd_mv,
|
||||
JSC_SCALL(fd_symlink,
|
||||
if (argc < 2)
|
||||
ret = JS_ThrowTypeError(js, "fd.symlink requires 2 arguments: target and link path");
|
||||
else if (!JS_IsString(argv[1]))
|
||||
else if (!JS_IsText(argv[1]))
|
||||
ret = JS_ThrowTypeError(js, "second argument must be a string (link path)");
|
||||
else {
|
||||
const char *link_path = JS_ToCString(js, argv[1]);
|
||||
@@ -502,10 +502,9 @@ JSC_SCALL(fd_readdir,
|
||||
ret = JS_ThrowInternalError(js, "FindFirstFile failed for %s", path);
|
||||
} else {
|
||||
ret = JS_NewArray(js);
|
||||
int i = 0;
|
||||
do {
|
||||
if (strcmp(ffd.cFileName, ".") == 0 || strcmp(ffd.cFileName, "..") == 0) continue;
|
||||
JS_SetPropertyUint32(js, ret, i++, JS_NewString(js, ffd.cFileName));
|
||||
JS_ArrayPush(js, ret,JS_NewString(js, ffd.cFileName));
|
||||
} while (FindNextFile(hFind, &ffd) != 0);
|
||||
FindClose(hFind);
|
||||
}
|
||||
@@ -515,10 +514,9 @@ JSC_SCALL(fd_readdir,
|
||||
d = opendir(str);
|
||||
if (d) {
|
||||
ret = JS_NewArray(js);
|
||||
int i = 0;
|
||||
while ((dir = readdir(d)) != NULL) {
|
||||
if (strcmp(dir->d_name, ".") == 0 || strcmp(dir->d_name, "..") == 0) continue;
|
||||
JS_SetPropertyUint32(js, ret, i++, JS_NewString(js, dir->d_name));
|
||||
JS_ArrayPush(js, ret, JS_NewString(js, dir->d_name));
|
||||
}
|
||||
closedir(d);
|
||||
} else {
|
||||
@@ -559,19 +557,22 @@ JSC_CCALL(fd_slurpwrite,
|
||||
size_t len;
|
||||
const char *data = js_get_blob_data(js, &len, argv[1]);
|
||||
|
||||
if (data == (const char *)-1)
|
||||
if (!data && len > 0)
|
||||
return JS_EXCEPTION;
|
||||
|
||||
const char *str = JS_ToCString(js, argv[0]);
|
||||
|
||||
if (!str) return JS_EXCEPTION;
|
||||
int fd = open(str, O_WRONLY | O_CREAT | O_TRUNC, 0644);
|
||||
JS_FreeCString(js, str);
|
||||
if (fd < 0)
|
||||
if (fd < 0) {
|
||||
JS_FreeCString(js, str);
|
||||
return JS_ThrowInternalError(js, "open failed for %s: %s", str, strerror(errno));
|
||||
}
|
||||
|
||||
ssize_t written = write(fd, data, len);
|
||||
close(fd);
|
||||
|
||||
JS_FreeCString(js, str);
|
||||
|
||||
if (written != (ssize_t)len)
|
||||
return JS_ThrowInternalError(js, "write failed for %s: %s", str, strerror(errno));
|
||||
|
||||
62
fd.cm
62
fd.cm
@@ -1,31 +1,67 @@
|
||||
var fd = this
|
||||
var wildstar = use('wildstar')
|
||||
|
||||
function last_pos(str, sep) {
|
||||
var last = null
|
||||
replace(str, sep, function(m, pos) {
|
||||
last = pos
|
||||
return m
|
||||
})
|
||||
return last
|
||||
}
|
||||
|
||||
// Helper to join paths
|
||||
function join_paths(base, rel) {
|
||||
base = base.replace(/\/+$/, "")
|
||||
rel = rel.replace(/^\/+/, "")
|
||||
base = replace(base, /\/+$/, "")
|
||||
rel = replace(rel, /^\/+/, "")
|
||||
if (!base) return rel
|
||||
if (!rel) return base
|
||||
return base + "/" + rel
|
||||
}
|
||||
|
||||
fd.join_paths = join_paths
|
||||
fd.basename = function basename(path) {
|
||||
var last = last_pos(path, '/')
|
||||
if (last == null) return path
|
||||
return text(path, last+1)
|
||||
}
|
||||
|
||||
fd.dirname = function dirname(path) {
|
||||
var last = last_pos(path, '/')
|
||||
if (last == null) return ""
|
||||
return text(path,0,last)
|
||||
}
|
||||
|
||||
fd.stem = function stem(path) {
|
||||
var last = last_pos(path, '.')
|
||||
if (last == null) return path
|
||||
return text(path,0,last)
|
||||
}
|
||||
|
||||
fd.globfs = function(globs, dir) {
|
||||
if (dir == null) dir = "."
|
||||
var results = []
|
||||
|
||||
function check_neg(path) {
|
||||
for (var g of globs) {
|
||||
if (g.startsWith("!") && wildstar.match(g.substring(1), path, wildstar.WM_WILDSTAR)) return true;
|
||||
}
|
||||
return false;
|
||||
var found = false;
|
||||
arrfor(globs, function(g) {
|
||||
if (starts_with(g, "!") && wildstar.match(text(g, 1), path, wildstar.WM_WILDSTAR)) {
|
||||
found = true;
|
||||
return true;
|
||||
}
|
||||
}, null, true);
|
||||
return found;
|
||||
}
|
||||
|
||||
function check_pos(path) {
|
||||
for (var g of globs) {
|
||||
if (!g.startsWith("!") && wildstar.match(g, path, wildstar.WM_WILDSTAR)) return true;
|
||||
}
|
||||
return false;
|
||||
var found = false;
|
||||
arrfor(globs, function(g) {
|
||||
if (!starts_with(g, "!") && wildstar.match(g, path, wildstar.WM_WILDSTAR)) {
|
||||
found = true;
|
||||
return true;
|
||||
}
|
||||
}, null, true);
|
||||
return found;
|
||||
}
|
||||
|
||||
function visit(curr_full, rel_prefix) {
|
||||
@@ -34,7 +70,7 @@ fd.globfs = function(globs, dir) {
|
||||
var list = fd.readdir(curr_full)
|
||||
if (!list) return
|
||||
|
||||
for (var item of list) {
|
||||
arrfor(list, function(item) {
|
||||
var item_rel = rel_prefix ? rel_prefix + "/" + item : item
|
||||
|
||||
var child_full = join_paths(curr_full, item)
|
||||
@@ -46,10 +82,10 @@ fd.globfs = function(globs, dir) {
|
||||
}
|
||||
} else {
|
||||
if (!check_neg(item_rel) && check_pos(item_rel)) {
|
||||
results.push(item_rel)
|
||||
push(results, item_rel)
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
var st = fd.stat(dir)
|
||||
|
||||
@@ -39,7 +39,7 @@ JSC_SCALL(fd_open,
|
||||
FileOptions flags = kFileRead;
|
||||
|
||||
// Parse optional flags argument
|
||||
if (argc > 1 && JS_IsString(argv[1])) {
|
||||
if (argc > 1 && JS_IsText(argv[1])) {
|
||||
const char *flag_str = JS_ToCString(js, argv[1]);
|
||||
flags = 0;
|
||||
|
||||
@@ -70,7 +70,7 @@ JSC_CCALL(fd_write,
|
||||
|
||||
size_t len;
|
||||
int wrote;
|
||||
if (JS_IsString(argv[1])) {
|
||||
if (JS_IsText(argv[1])) {
|
||||
const char *data = JS_ToCStringLen(js, &len, argv[1]);
|
||||
if (!data) return JS_EXCEPTION;
|
||||
wrote = pd_file->write(fd, data, (unsigned int)len);
|
||||
@@ -202,7 +202,7 @@ JSC_SCALL(fd_mkdir,
|
||||
JSC_SCALL(fd_mv,
|
||||
if (argc < 2)
|
||||
ret = JS_ThrowTypeError(js, "fd.mv requires 2 arguments: old path and new path");
|
||||
else if (!JS_IsString(argv[1]))
|
||||
else if (!JS_IsText(argv[1]))
|
||||
ret = JS_ThrowTypeError(js, "second argument must be a string (new path)");
|
||||
else {
|
||||
const char *new_path = JS_ToCString(js, argv[1]);
|
||||
@@ -216,7 +216,7 @@ JSC_SCALL(fd_mv,
|
||||
|
||||
JSC_SCALL(fd_symlink,
|
||||
// Not supported
|
||||
if (argc >= 2 && JS_IsString(argv[1])) {
|
||||
if (argc >= 2 && JS_IsText(argv[1])) {
|
||||
// consume arg
|
||||
JS_FreeCString(js, JS_ToCString(js, argv[1]));
|
||||
}
|
||||
|
||||
67
fetch.ce
67
fetch.ce
@@ -13,7 +13,7 @@ var shop = use('internal/shop')
|
||||
// Parse arguments
|
||||
var target_pkg = null
|
||||
|
||||
for (var i = 0; i < args.length; i++) {
|
||||
for (var i = 0; i < length(args); i++) {
|
||||
if (args[i] == '--help' || args[i] == '-h') {
|
||||
log.console("Usage: cell fetch [package]")
|
||||
log.console("Fetch package zips from remote sources.")
|
||||
@@ -24,7 +24,7 @@ for (var i = 0; i < args.length; i++) {
|
||||
log.console("This command ensures that the zip files on disk match what's in")
|
||||
log.console("the lock file. For local packages, this is a no-op.")
|
||||
$stop()
|
||||
} else if (!args[i].startsWith('-')) {
|
||||
} else if (!starts_with(args[i], '-')) {
|
||||
target_pkg = args[i]
|
||||
}
|
||||
}
|
||||
@@ -35,52 +35,55 @@ var packages_to_fetch = []
|
||||
|
||||
if (target_pkg) {
|
||||
// Fetch specific package
|
||||
if (!all_packages.includes(target_pkg)) {
|
||||
if (find(all_packages, target_pkg) == null) {
|
||||
log.error("Package not found: " + target_pkg)
|
||||
$stop()
|
||||
}
|
||||
packages_to_fetch.push(target_pkg)
|
||||
push(packages_to_fetch, target_pkg)
|
||||
} else {
|
||||
// Fetch all packages
|
||||
packages_to_fetch = all_packages
|
||||
}
|
||||
|
||||
log.console("Fetching " + text(packages_to_fetch.length) + " package(s)...")
|
||||
var remote_count = 0
|
||||
arrfor(packages_to_fetch, function(pkg) {
|
||||
var entry = lock[pkg]
|
||||
if (pkg != 'core' && (!entry || entry.type != 'local'))
|
||||
remote_count++
|
||||
}, null, null)
|
||||
|
||||
var success_count = 0
|
||||
var skip_count = 0
|
||||
if (remote_count > 0)
|
||||
log.console(`Fetching ${text(remote_count)} remote package(s)...`)
|
||||
|
||||
var downloaded_count = 0
|
||||
var cached_count = 0
|
||||
var fail_count = 0
|
||||
|
||||
for (var pkg of packages_to_fetch) {
|
||||
var entry = lock[pkg]
|
||||
|
||||
// Skip local packages
|
||||
if (entry && entry.type == 'local') {
|
||||
skip_count++
|
||||
continue
|
||||
}
|
||||
|
||||
arrfor(packages_to_fetch, function(pkg) {
|
||||
// Skip core (handled separately)
|
||||
if (pkg == 'core') {
|
||||
skip_count++
|
||||
continue
|
||||
}
|
||||
|
||||
if (pkg == 'core') return
|
||||
|
||||
var result = shop.fetch(pkg)
|
||||
if (result) {
|
||||
if (result.zip_blob) {
|
||||
log.console("Fetched: " + pkg)
|
||||
success_count++
|
||||
} else {
|
||||
skip_count++
|
||||
}
|
||||
} else {
|
||||
log.error("Failed to fetch: " + pkg)
|
||||
if (result.status == 'local') {
|
||||
// Local packages are just symlinks, nothing to fetch
|
||||
return
|
||||
} else if (result.status == 'cached') {
|
||||
cached_count++
|
||||
} else if (result.status == 'downloaded') {
|
||||
log.console(" Downloaded: " + pkg)
|
||||
downloaded_count++
|
||||
} else if (result.status == 'error') {
|
||||
log.error(" Failed: " + pkg + (result.message ? " - " + result.message : ""))
|
||||
fail_count++
|
||||
}
|
||||
}
|
||||
}, null, null)
|
||||
|
||||
log.console("")
|
||||
log.console("Fetch complete: " + text(success_count) + " fetched, " + text(skip_count) + " skipped, " + text(fail_count) + " failed")
|
||||
var parts = []
|
||||
if (downloaded_count > 0) push(parts, `${text(downloaded_count)} downloaded`)
|
||||
if (cached_count > 0) push(parts, `${text(cached_count)} cached`)
|
||||
if (fail_count > 0) push(parts, `${text(fail_count)} failed`)
|
||||
if (length(parts) == 0) push(parts, "nothing to fetch")
|
||||
log.console("Fetch complete: " + text(parts, ", "))
|
||||
|
||||
$stop()
|
||||
|
||||
13
fold.ce
Normal file
13
fold.ce
Normal file
@@ -0,0 +1,13 @@
|
||||
var fd = use("fd")
|
||||
var json = use("json")
|
||||
|
||||
var filename = args[0]
|
||||
var src = text(fd.slurp(filename))
|
||||
var tokenize = use("tokenize")
|
||||
var parse = use("parse")
|
||||
var fold = use("fold")
|
||||
|
||||
var tok_result = tokenize(src, filename)
|
||||
var ast = parse(tok_result.tokens, src, filename, tokenize)
|
||||
var folded = fold(ast)
|
||||
print(json.encode(folded))
|
||||
968
fold.cm
Normal file
968
fold.cm
Normal file
@@ -0,0 +1,968 @@
|
||||
// fold.cm — AST optimization pass
|
||||
// Constant folding, constant propagation, dead code elimination
|
||||
|
||||
var fold = function(ast) {
|
||||
var scopes = ast.scopes
|
||||
var nr_scopes = length(scopes)
|
||||
|
||||
// ============================================================
|
||||
// Helpers
|
||||
// ============================================================
|
||||
|
||||
var is_literal = function(expr) {
|
||||
if (expr == null) return false
|
||||
var k = expr.kind
|
||||
return k == "number" || k == "text" || k == "true" || k == "false" || k == "null"
|
||||
}
|
||||
|
||||
var is_pure = function(expr) {
|
||||
if (expr == null) return true
|
||||
var k = expr.kind
|
||||
var i = 0
|
||||
if (k == "number" || k == "text" || k == "true" || k == "false" ||
|
||||
k == "null" || k == "name" || k == "this") return true
|
||||
if (k == "function") return true
|
||||
if (k == "!" || k == "~" || k == "-unary" || k == "+unary") {
|
||||
return is_pure(expr.expression)
|
||||
}
|
||||
if (k == "array") {
|
||||
i = 0
|
||||
while (i < length(expr.list)) {
|
||||
if (!is_pure(expr.list[i])) return false
|
||||
i = i + 1
|
||||
}
|
||||
return true
|
||||
}
|
||||
if (k == "record") {
|
||||
i = 0
|
||||
while (i < length(expr.list)) {
|
||||
if (!is_pure(expr.list[i].right)) return false
|
||||
i = i + 1
|
||||
}
|
||||
return true
|
||||
}
|
||||
if (k == "then") {
|
||||
return is_pure(expr.expression) && is_pure(expr.then) && is_pure(expr.else)
|
||||
}
|
||||
if (k == "==" || k == "!=" || k == "&&" || k == "||") {
|
||||
return is_pure(expr.left) && is_pure(expr.right)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
var copy_loc = function(from, to) {
|
||||
to.at = from.at
|
||||
to.from_row = from.from_row
|
||||
to.from_column = from.from_column
|
||||
to.to_row = from.to_row
|
||||
to.to_column = from.to_column
|
||||
return to
|
||||
}
|
||||
|
||||
var make_number = function(val, src) {
|
||||
return copy_loc(src, {kind: "number", value: text(val), number: val})
|
||||
}
|
||||
|
||||
var make_text = function(val, src) {
|
||||
return copy_loc(src, {kind: "text", value: val})
|
||||
}
|
||||
|
||||
var make_bool = function(val, src) {
|
||||
if (val) return copy_loc(src, {kind: "true"})
|
||||
return copy_loc(src, {kind: "false"})
|
||||
}
|
||||
|
||||
var make_null = function(src) {
|
||||
return copy_loc(src, {kind: "null"})
|
||||
}
|
||||
|
||||
var is_truthy_literal = function(expr) {
|
||||
if (expr == null) return null
|
||||
var k = expr.kind
|
||||
var nv = null
|
||||
if (k == "true") return true
|
||||
if (k == "false" || k == "null") return false
|
||||
if (k == "number") {
|
||||
nv = expr.number
|
||||
if (nv == null) nv = number(expr.value)
|
||||
return nv != 0
|
||||
}
|
||||
if (k == "text") return length(expr.value) > 0
|
||||
return null
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Scope helpers
|
||||
// ============================================================
|
||||
|
||||
var find_scope = function(fn_nr) {
|
||||
var i = 0
|
||||
while (i < nr_scopes) {
|
||||
if (scopes[i].function_nr == fn_nr) return scopes[i]
|
||||
i = i + 1
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
var scope_var = function(fn_nr, name) {
|
||||
var sc = find_scope(fn_nr)
|
||||
if (sc == null) return null
|
||||
return sc[name]
|
||||
}
|
||||
|
||||
var remove_scope_var = function(fn_nr, name) {
|
||||
var sc = find_scope(fn_nr)
|
||||
if (sc == null) return null
|
||||
delete sc[name]
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Pass 1: pre-scan for constants and function arities
|
||||
// ============================================================
|
||||
|
||||
var const_defs = {}
|
||||
var fn_arities = {}
|
||||
|
||||
var register_const = function(fn_nr, name, lit_node) {
|
||||
var key = text(fn_nr)
|
||||
if (const_defs[key] == null) const_defs[key] = {}
|
||||
const_defs[key][name] = lit_node
|
||||
}
|
||||
|
||||
var get_const = function(fn_nr, name) {
|
||||
var key = text(fn_nr)
|
||||
if (const_defs[key] == null) return null
|
||||
return const_defs[key][name]
|
||||
}
|
||||
|
||||
var register_arity = function(fn_nr, name, count) {
|
||||
var key = text(fn_nr)
|
||||
if (fn_arities[key] == null) fn_arities[key] = {}
|
||||
fn_arities[key][name] = count
|
||||
}
|
||||
|
||||
var pre_scan_stmts = null
|
||||
var pre_scan_fn = null
|
||||
|
||||
pre_scan_fn = function(node) {
|
||||
if (node == null) return null
|
||||
if (node.statements != null) pre_scan_stmts(node.statements, node.function_nr)
|
||||
if (node.disruption != null) pre_scan_stmts(node.disruption, node.function_nr)
|
||||
}
|
||||
|
||||
pre_scan_stmts = function(stmts, fn_nr) {
|
||||
var i = 0
|
||||
var j = 0
|
||||
var stmt = null
|
||||
var kind = null
|
||||
var name = null
|
||||
var sv = null
|
||||
var item = null
|
||||
while (i < length(stmts)) {
|
||||
stmt = stmts[i]
|
||||
kind = stmt.kind
|
||||
if (kind == "def") {
|
||||
name = stmt.left.name
|
||||
if (name != null && is_literal(stmt.right)) {
|
||||
sv = scope_var(fn_nr, name)
|
||||
if (sv != null && !sv.closure) {
|
||||
register_const(fn_nr, name, stmt.right)
|
||||
}
|
||||
}
|
||||
} else if (kind == "function") {
|
||||
name = stmt.name
|
||||
if (name != null && stmt.arity != null) {
|
||||
register_arity(fn_nr, name, stmt.arity)
|
||||
}
|
||||
pre_scan_fn(stmt)
|
||||
} else if (kind == "var") {
|
||||
if (stmt.right != null && stmt.right.kind == "function" && stmt.right.arity != null) {
|
||||
name = stmt.left.name
|
||||
if (name != null) {
|
||||
sv = scope_var(fn_nr, name)
|
||||
if (sv != null && sv.make == "var") {
|
||||
register_arity(fn_nr, name, stmt.right.arity)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (kind == "var_list") {
|
||||
j = 0
|
||||
while (j < length(stmt.list)) {
|
||||
item = stmt.list[j]
|
||||
if (item.kind == "var" && item.right != null && item.right.kind == "function" && item.right.arity != null) {
|
||||
name = item.left.name
|
||||
if (name != null) {
|
||||
sv = scope_var(fn_nr, name)
|
||||
if (sv != null && sv.make == "var") {
|
||||
register_arity(fn_nr, name, item.right.arity)
|
||||
}
|
||||
}
|
||||
}
|
||||
j = j + 1
|
||||
}
|
||||
}
|
||||
i = i + 1
|
||||
}
|
||||
}
|
||||
|
||||
var pre_scan_expr_fns = null
|
||||
pre_scan_expr_fns = function(expr) {
|
||||
if (expr == null) return null
|
||||
var k = expr.kind
|
||||
var i = 0
|
||||
if (k == "function") {
|
||||
pre_scan_fn(expr)
|
||||
}
|
||||
if (expr.left != null) pre_scan_expr_fns(expr.left)
|
||||
if (expr.right != null) pre_scan_expr_fns(expr.right)
|
||||
if (expr.expression != null) pre_scan_expr_fns(expr.expression)
|
||||
if (expr.then != null) pre_scan_expr_fns(expr.then)
|
||||
if (expr.else != null) pre_scan_expr_fns(expr.else)
|
||||
if (k == "(" || k == "array") {
|
||||
i = 0
|
||||
while (i < length(expr.list)) {
|
||||
pre_scan_expr_fns(expr.list[i])
|
||||
i = i + 1
|
||||
}
|
||||
}
|
||||
if (k == "record") {
|
||||
i = 0
|
||||
while (i < length(expr.list)) {
|
||||
pre_scan_expr_fns(expr.list[i].right)
|
||||
i = i + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var pre_scan_stmt_exprs = null
|
||||
pre_scan_stmt_exprs = function(stmts, fn_nr) {
|
||||
var i = 0
|
||||
var j = 0
|
||||
var stmt = null
|
||||
var kind = null
|
||||
while (i < length(stmts)) {
|
||||
stmt = stmts[i]
|
||||
kind = stmt.kind
|
||||
if (kind == "var" || kind == "def") {
|
||||
pre_scan_expr_fns(stmt.right)
|
||||
} else if (kind == "var_list") {
|
||||
j = 0
|
||||
while (j < length(stmt.list)) {
|
||||
pre_scan_expr_fns(stmt.list[j].right)
|
||||
j = j + 1
|
||||
}
|
||||
} else if (kind == "call") {
|
||||
pre_scan_expr_fns(stmt.expression)
|
||||
} else if (kind == "if") {
|
||||
pre_scan_expr_fns(stmt.expression)
|
||||
pre_scan_stmt_exprs(stmt.then, fn_nr)
|
||||
pre_scan_stmt_exprs(stmt.list, fn_nr)
|
||||
if (stmt.else != null) pre_scan_stmt_exprs(stmt.else, fn_nr)
|
||||
} else if (kind == "while" || kind == "do") {
|
||||
pre_scan_expr_fns(stmt.expression)
|
||||
pre_scan_stmt_exprs(stmt.statements, fn_nr)
|
||||
} else if (kind == "for") {
|
||||
if (stmt.init != null) {
|
||||
if (stmt.init.kind == "var" || stmt.init.kind == "def") {
|
||||
pre_scan_expr_fns(stmt.init.right)
|
||||
} else {
|
||||
pre_scan_expr_fns(stmt.init)
|
||||
}
|
||||
}
|
||||
pre_scan_expr_fns(stmt.test)
|
||||
pre_scan_expr_fns(stmt.update)
|
||||
pre_scan_stmt_exprs(stmt.statements, fn_nr)
|
||||
} else if (kind == "return" || kind == "go") {
|
||||
pre_scan_expr_fns(stmt.expression)
|
||||
} else if (kind == "block") {
|
||||
pre_scan_stmt_exprs(stmt.statements, fn_nr)
|
||||
} else if (kind == "label") {
|
||||
if (stmt.statement != null) {
|
||||
pre_scan_stmt_exprs([stmt.statement], fn_nr)
|
||||
}
|
||||
} else if (kind == "function") {
|
||||
// already handled in pre_scan_stmts
|
||||
null
|
||||
}
|
||||
i = i + 1
|
||||
}
|
||||
}
|
||||
|
||||
var pre_scan = function() {
|
||||
pre_scan_stmts(ast.statements, 0)
|
||||
pre_scan_stmts(ast.functions, 0)
|
||||
pre_scan_stmt_exprs(ast.statements, 0)
|
||||
pre_scan_stmt_exprs(ast.functions, 0)
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Pass 2: fold expressions and statements
|
||||
// ============================================================
|
||||
|
||||
var fold_expr = null
|
||||
var fold_stmt = null
|
||||
var fold_stmts = null
|
||||
|
||||
fold_expr = function(expr, fn_nr) {
|
||||
if (expr == null) return null
|
||||
var k = expr.kind
|
||||
var left = null
|
||||
var right = null
|
||||
var lv = null
|
||||
var rv = null
|
||||
var result = null
|
||||
var i = 0
|
||||
var sv = null
|
||||
var lit = null
|
||||
var cond_k = null
|
||||
var ek = null
|
||||
var target = null
|
||||
var ar = null
|
||||
var akey = null
|
||||
var tv = null
|
||||
|
||||
// Recurse into children first (bottom-up)
|
||||
if (k == "+" || k == "-" || k == "*" || k == "/" || k == "%" ||
|
||||
k == "**" || k == "==" || k == "!=" || k == "<" || k == ">" ||
|
||||
k == "<=" || k == ">=" || k == "&" || k == "|" || k == "^" ||
|
||||
k == "<<" || k == ">>" || k == ">>>" || k == "&&" || k == "||" ||
|
||||
k == "," || k == "in") {
|
||||
expr.left = fold_expr(expr.left, fn_nr)
|
||||
expr.right = fold_expr(expr.right, fn_nr)
|
||||
} else if (k == "." || k == "[") {
|
||||
expr.left = fold_expr(expr.left, fn_nr)
|
||||
if (k == "[" && expr.right != null) expr.right = fold_expr(expr.right, fn_nr)
|
||||
} else if (k == "!" || k == "~" || k == "-unary" || k == "+unary" || k == "delete") {
|
||||
expr.expression = fold_expr(expr.expression, fn_nr)
|
||||
} else if (k == "++" || k == "--") {
|
||||
return expr
|
||||
} else if (k == "then") {
|
||||
expr.expression = fold_expr(expr.expression, fn_nr)
|
||||
expr.then = fold_expr(expr.then, fn_nr)
|
||||
expr.else = fold_expr(expr.else, fn_nr)
|
||||
} else if (k == "(") {
|
||||
expr.expression = fold_expr(expr.expression, fn_nr)
|
||||
i = 0
|
||||
while (i < length(expr.list)) {
|
||||
expr.list[i] = fold_expr(expr.list[i], fn_nr)
|
||||
i = i + 1
|
||||
}
|
||||
} else if (k == "array") {
|
||||
i = 0
|
||||
while (i < length(expr.list)) {
|
||||
expr.list[i] = fold_expr(expr.list[i], fn_nr)
|
||||
i = i + 1
|
||||
}
|
||||
} else if (k == "record") {
|
||||
i = 0
|
||||
while (i < length(expr.list)) {
|
||||
expr.list[i].right = fold_expr(expr.list[i].right, fn_nr)
|
||||
i = i + 1
|
||||
}
|
||||
} else if (k == "text literal") {
|
||||
i = 0
|
||||
while (i < length(expr.list)) {
|
||||
expr.list[i] = fold_expr(expr.list[i], fn_nr)
|
||||
i = i + 1
|
||||
}
|
||||
} else if (k == "function") {
|
||||
fold_fn(expr)
|
||||
return expr
|
||||
} else if (k == "assign" || k == "+=" || k == "-=" || k == "*=" ||
|
||||
k == "/=" || k == "%=" || k == "<<=" || k == ">>=" ||
|
||||
k == ">>>=" || k == "&=" || k == "^=" || k == "|=" ||
|
||||
k == "**=" || k == "&&=" || k == "||=") {
|
||||
expr.right = fold_expr(expr.right, fn_nr)
|
||||
return expr
|
||||
}
|
||||
|
||||
// Constant propagation: name → literal
|
||||
if (k == "name" && expr.level == 0) {
|
||||
lit = get_const(fn_nr, expr.name)
|
||||
if (lit != null) {
|
||||
sv = scope_var(fn_nr, expr.name)
|
||||
if (sv != null && !sv.closure) {
|
||||
return copy_loc(expr, {kind: lit.kind, value: lit.value, number: lit.number})
|
||||
}
|
||||
}
|
||||
return expr
|
||||
}
|
||||
|
||||
// Binary constant folding
|
||||
if (k == "+" || k == "-" || k == "*" || k == "/" || k == "%" || k == "**") {
|
||||
left = expr.left
|
||||
right = expr.right
|
||||
if (left != null && right != null && left.kind == "number" && right.kind == "number") {
|
||||
lv = left.number
|
||||
rv = right.number
|
||||
if (lv == null) lv = number(left.value)
|
||||
if (rv == null) rv = number(right.value)
|
||||
if (k == "/") {
|
||||
if (rv == 0) return make_null(expr)
|
||||
}
|
||||
if (k == "%") {
|
||||
if (rv == 0) return make_null(expr)
|
||||
}
|
||||
result = null
|
||||
if (k == "+") result = lv + rv
|
||||
else if (k == "-") result = lv - rv
|
||||
else if (k == "*") result = lv * rv
|
||||
else if (k == "/") result = lv / rv
|
||||
else if (k == "%") result = lv % rv
|
||||
else if (k == "**") result = lv ** rv
|
||||
if (result == null) return make_null(expr)
|
||||
return make_number(result, expr)
|
||||
}
|
||||
// text + text
|
||||
if (k == "+" && left != null && right != null && left.kind == "text" && right.kind == "text") {
|
||||
return make_text(left.value + right.value, expr)
|
||||
}
|
||||
return expr
|
||||
}
|
||||
|
||||
// Comparison folding
|
||||
if (k == "==" || k == "!=" || k == "<" || k == ">" || k == "<=" || k == ">=") {
|
||||
left = expr.left
|
||||
right = expr.right
|
||||
if (left != null && right != null) {
|
||||
if (left.kind == "number" && right.kind == "number") {
|
||||
lv = left.number
|
||||
rv = right.number
|
||||
if (lv == null) lv = number(left.value)
|
||||
if (rv == null) rv = number(right.value)
|
||||
if (k == "==") return make_bool(lv == rv, expr)
|
||||
if (k == "!=") return make_bool(lv != rv, expr)
|
||||
if (k == "<") return make_bool(lv < rv, expr)
|
||||
if (k == ">") return make_bool(lv > rv, expr)
|
||||
if (k == "<=") return make_bool(lv <= rv, expr)
|
||||
if (k == ">=") return make_bool(lv >= rv, expr)
|
||||
}
|
||||
if (left.kind == "text" && right.kind == "text") {
|
||||
if (k == "==") return make_bool(left.value == right.value, expr)
|
||||
if (k == "!=") return make_bool(left.value != right.value, expr)
|
||||
}
|
||||
}
|
||||
return expr
|
||||
}
|
||||
|
||||
// Bitwise folding
|
||||
if (k == "&" || k == "|" || k == "^" || k == "<<" || k == ">>") {
|
||||
left = expr.left
|
||||
right = expr.right
|
||||
if (left != null && right != null && left.kind == "number" && right.kind == "number") {
|
||||
lv = left.number
|
||||
rv = right.number
|
||||
if (lv == null) lv = number(left.value)
|
||||
if (rv == null) rv = number(right.value)
|
||||
if (k == "&") return make_number(lv & rv, expr)
|
||||
if (k == "|") return make_number(lv | rv, expr)
|
||||
if (k == "^") return make_number(lv ^ rv, expr)
|
||||
if (k == "<<") return make_number(lv << rv, expr)
|
||||
if (k == ">>") return make_number(lv >> rv, expr)
|
||||
}
|
||||
return expr
|
||||
}
|
||||
|
||||
// Unary folding
|
||||
if (k == "!") {
|
||||
if (expr.expression != null) {
|
||||
ek = expr.expression.kind
|
||||
if (ek == "true") return make_bool(false, expr)
|
||||
if (ek == "false") return make_bool(true, expr)
|
||||
}
|
||||
return expr
|
||||
}
|
||||
if (k == "~") {
|
||||
if (expr.expression != null && expr.expression.kind == "number") {
|
||||
lv = expr.expression.number
|
||||
if (lv == null) lv = number(expr.expression.value)
|
||||
return make_number(~lv, expr)
|
||||
}
|
||||
return expr
|
||||
}
|
||||
if (k == "-unary") {
|
||||
if (expr.expression != null && expr.expression.kind == "number") {
|
||||
lv = expr.expression.number
|
||||
if (lv == null) lv = number(expr.expression.value)
|
||||
return make_number(0 - lv, expr)
|
||||
}
|
||||
return expr
|
||||
}
|
||||
|
||||
// Ternary with literal condition
|
||||
if (k == "then") {
|
||||
tv = is_truthy_literal(expr.expression)
|
||||
if (tv == true) return expr.then
|
||||
if (tv == false) return expr.else
|
||||
return expr
|
||||
}
|
||||
|
||||
// Call: stamp arity
|
||||
if (k == "(") {
|
||||
target = expr.expression
|
||||
if (target != null && target.kind == "name" && target.level == 0) {
|
||||
ar = null
|
||||
akey = text(fn_nr)
|
||||
if (fn_arities[akey] != null) ar = fn_arities[akey][target.name]
|
||||
if (ar != null) expr.arity = ar
|
||||
}
|
||||
return expr
|
||||
}
|
||||
|
||||
return expr
|
||||
}
|
||||
|
||||
var fold_fn = null
|
||||
|
||||
fold_stmt = function(stmt, fn_nr) {
|
||||
if (stmt == null) return null
|
||||
var k = stmt.kind
|
||||
var i = 0
|
||||
var sv = null
|
||||
var cond_k = null
|
||||
var ik = null
|
||||
var tv = null
|
||||
|
||||
if (k == "var" || k == "def") {
|
||||
stmt.right = fold_expr(stmt.right, fn_nr)
|
||||
return stmt
|
||||
}
|
||||
if (k == "var_list") {
|
||||
i = 0
|
||||
while (i < length(stmt.list)) {
|
||||
stmt.list[i] = fold_stmt(stmt.list[i], fn_nr)
|
||||
i = i + 1
|
||||
}
|
||||
return stmt
|
||||
}
|
||||
if (k == "call") {
|
||||
stmt.expression = fold_expr(stmt.expression, fn_nr)
|
||||
return stmt
|
||||
}
|
||||
if (k == "if") {
|
||||
stmt.expression = fold_expr(stmt.expression, fn_nr)
|
||||
tv = is_truthy_literal(stmt.expression)
|
||||
if (tv == true) {
|
||||
stmt.then = fold_stmts(stmt.then, fn_nr)
|
||||
return {kind: "block", statements: stmt.then,
|
||||
at: stmt.at, from_row: stmt.from_row, from_column: stmt.from_column,
|
||||
to_row: stmt.to_row, to_column: stmt.to_column}
|
||||
}
|
||||
if (tv == false) {
|
||||
if (stmt.else != null && length(stmt.else) > 0) {
|
||||
stmt.else = fold_stmts(stmt.else, fn_nr)
|
||||
return {kind: "block", statements: stmt.else,
|
||||
at: stmt.at, from_row: stmt.from_row, from_column: stmt.from_column,
|
||||
to_row: stmt.to_row, to_column: stmt.to_column}
|
||||
}
|
||||
if (stmt.list != null && length(stmt.list) > 0) {
|
||||
return fold_stmt(stmt.list[0], fn_nr)
|
||||
}
|
||||
return null
|
||||
}
|
||||
stmt.then = fold_stmts(stmt.then, fn_nr)
|
||||
stmt.list = fold_stmts(stmt.list, fn_nr)
|
||||
if (stmt.else != null) stmt.else = fold_stmts(stmt.else, fn_nr)
|
||||
return stmt
|
||||
}
|
||||
if (k == "while") {
|
||||
stmt.expression = fold_expr(stmt.expression, fn_nr)
|
||||
if (stmt.expression.kind == "false" || stmt.expression.kind == "null") return null
|
||||
stmt.statements = fold_stmts(stmt.statements, fn_nr)
|
||||
return stmt
|
||||
}
|
||||
if (k == "do") {
|
||||
stmt.statements = fold_stmts(stmt.statements, fn_nr)
|
||||
stmt.expression = fold_expr(stmt.expression, fn_nr)
|
||||
return stmt
|
||||
}
|
||||
if (k == "for") {
|
||||
if (stmt.init != null) {
|
||||
ik = stmt.init.kind
|
||||
if (ik == "var" || ik == "def") {
|
||||
stmt.init = fold_stmt(stmt.init, fn_nr)
|
||||
} else {
|
||||
stmt.init = fold_expr(stmt.init, fn_nr)
|
||||
}
|
||||
}
|
||||
if (stmt.test != null) stmt.test = fold_expr(stmt.test, fn_nr)
|
||||
if (stmt.update != null) stmt.update = fold_expr(stmt.update, fn_nr)
|
||||
stmt.statements = fold_stmts(stmt.statements, fn_nr)
|
||||
return stmt
|
||||
}
|
||||
if (k == "return" || k == "go") {
|
||||
stmt.expression = fold_expr(stmt.expression, fn_nr)
|
||||
return stmt
|
||||
}
|
||||
if (k == "block") {
|
||||
stmt.statements = fold_stmts(stmt.statements, fn_nr)
|
||||
return stmt
|
||||
}
|
||||
if (k == "label") {
|
||||
stmt.statement = fold_stmt(stmt.statement, fn_nr)
|
||||
return stmt
|
||||
}
|
||||
if (k == "function") {
|
||||
fold_fn(stmt)
|
||||
return stmt
|
||||
}
|
||||
return stmt
|
||||
}
|
||||
|
||||
fold_stmts = function(stmts, fn_nr) {
|
||||
var i = 0
|
||||
var stmt = null
|
||||
var out = []
|
||||
var sv = null
|
||||
var name = null
|
||||
while (i < length(stmts)) {
|
||||
stmt = fold_stmt(stmts[i], fn_nr)
|
||||
if (stmt == null) {
|
||||
i = i + 1
|
||||
continue
|
||||
}
|
||||
// Dead code elimination: unused pure var/def
|
||||
if (stmt.kind == "var" || stmt.kind == "def") {
|
||||
name = stmt.left.name
|
||||
if (name != null) {
|
||||
sv = scope_var(fn_nr, name)
|
||||
if (sv != null && sv.nr_uses == 0 && is_pure(stmt.right)) {
|
||||
stmt.dead = true
|
||||
}
|
||||
}
|
||||
}
|
||||
// Dead function elimination
|
||||
if (stmt.kind == "function" && stmt.name != null) {
|
||||
sv = scope_var(fn_nr, stmt.name)
|
||||
if (sv != null && sv.nr_uses == 0) {
|
||||
stmt.dead = true
|
||||
}
|
||||
}
|
||||
if (stmt.dead != true) push(out, stmt)
|
||||
i = i + 1
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
fold_fn = function(node) {
|
||||
if (node == null) return null
|
||||
var fn_nr = node.function_nr
|
||||
if (fn_nr == null) return null
|
||||
// Fold param defaults
|
||||
var i = 0
|
||||
while (i < length(node.list)) {
|
||||
if (node.list[i].expression != null) {
|
||||
node.list[i].expression = fold_expr(node.list[i].expression, fn_nr)
|
||||
}
|
||||
i = i + 1
|
||||
}
|
||||
if (node.statements != null) node.statements = fold_stmts(node.statements, fn_nr)
|
||||
if (node.disruption != null) node.disruption = fold_stmts(node.disruption, fn_nr)
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Pass 3: cleanup scopes
|
||||
// ============================================================
|
||||
|
||||
var cleanup = function() {
|
||||
var i = 0
|
||||
var sc = null
|
||||
var keys = null
|
||||
var j = 0
|
||||
var key = null
|
||||
var entry = null
|
||||
var slots = 0
|
||||
var close_slots = 0
|
||||
|
||||
// Remove dead vars from scope records and recalculate slot counts
|
||||
while (i < nr_scopes) {
|
||||
sc = scopes[i]
|
||||
keys = array(sc)
|
||||
slots = 0
|
||||
close_slots = 0
|
||||
j = 0
|
||||
while (j < length(keys)) {
|
||||
key = keys[j]
|
||||
if (key != "function_nr") {
|
||||
entry = sc[key]
|
||||
if (entry != null && entry.nr_uses == 0 && entry.make != "input" && entry.make != "function") {
|
||||
delete sc[key]
|
||||
} else if (entry != null) {
|
||||
slots = slots + 1
|
||||
if (entry.closure) close_slots = close_slots + 1
|
||||
}
|
||||
}
|
||||
j = j + 1
|
||||
}
|
||||
i = i + 1
|
||||
}
|
||||
|
||||
// Update nr_slots and nr_close_slots on function nodes
|
||||
var update_fn_slots = null
|
||||
update_fn_slots = function(node) {
|
||||
if (node == null) return null
|
||||
var fn_nr = node.function_nr
|
||||
if (fn_nr == null) return null
|
||||
var sc = find_scope(fn_nr)
|
||||
if (sc == null) return null
|
||||
var keys = array(sc)
|
||||
var s = 0
|
||||
var cs = 0
|
||||
var ki = 0
|
||||
var ent = null
|
||||
while (ki < length(keys)) {
|
||||
if (keys[ki] != "function_nr") {
|
||||
ent = sc[keys[ki]]
|
||||
if (ent != null) {
|
||||
s = s + 1
|
||||
if (ent.closure) cs = cs + 1
|
||||
}
|
||||
}
|
||||
ki = ki + 1
|
||||
}
|
||||
node.nr_slots = s
|
||||
node.nr_close_slots = cs
|
||||
}
|
||||
|
||||
var walk_stmts_for_fns = null
|
||||
var walk_expr_for_fns = null
|
||||
|
||||
walk_expr_for_fns = function(expr) {
|
||||
if (expr == null) return null
|
||||
var k = expr.kind
|
||||
var i = 0
|
||||
if (k == "function") {
|
||||
update_fn_slots(expr)
|
||||
walk_stmts_for_fns(expr.statements)
|
||||
walk_stmts_for_fns(expr.disruption)
|
||||
return null
|
||||
}
|
||||
if (expr.left != null) walk_expr_for_fns(expr.left)
|
||||
if (expr.right != null) walk_expr_for_fns(expr.right)
|
||||
if (expr.expression != null) walk_expr_for_fns(expr.expression)
|
||||
if (expr.then != null) walk_expr_for_fns(expr.then)
|
||||
if (expr.else != null) walk_expr_for_fns(expr.else)
|
||||
if (k == "(" || k == "array" || k == "text literal") {
|
||||
i = 0
|
||||
while (i < length(expr.list)) {
|
||||
walk_expr_for_fns(expr.list[i])
|
||||
i = i + 1
|
||||
}
|
||||
}
|
||||
if (k == "record") {
|
||||
i = 0
|
||||
while (i < length(expr.list)) {
|
||||
walk_expr_for_fns(expr.list[i].right)
|
||||
i = i + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
walk_stmts_for_fns = function(stmts) {
|
||||
if (stmts == null) return null
|
||||
var i = 0
|
||||
var j = 0
|
||||
var stmt = null
|
||||
var k = null
|
||||
while (i < length(stmts)) {
|
||||
stmt = stmts[i]
|
||||
k = stmt.kind
|
||||
if (k == "function") {
|
||||
update_fn_slots(stmt)
|
||||
walk_stmts_for_fns(stmt.statements)
|
||||
walk_stmts_for_fns(stmt.disruption)
|
||||
} else if (k == "var" || k == "def") {
|
||||
walk_expr_for_fns(stmt.right)
|
||||
} else if (k == "var_list") {
|
||||
j = 0
|
||||
while (j < length(stmt.list)) {
|
||||
walk_expr_for_fns(stmt.list[j].right)
|
||||
j = j + 1
|
||||
}
|
||||
} else if (k == "call") {
|
||||
walk_expr_for_fns(stmt.expression)
|
||||
} else if (k == "if") {
|
||||
walk_expr_for_fns(stmt.expression)
|
||||
walk_stmts_for_fns(stmt.then)
|
||||
walk_stmts_for_fns(stmt.list)
|
||||
if (stmt.else != null) walk_stmts_for_fns(stmt.else)
|
||||
} else if (k == "while" || k == "do") {
|
||||
walk_expr_for_fns(stmt.expression)
|
||||
walk_stmts_for_fns(stmt.statements)
|
||||
} else if (k == "for") {
|
||||
if (stmt.init != null) {
|
||||
if (stmt.init.kind == "var" || stmt.init.kind == "def") {
|
||||
walk_expr_for_fns(stmt.init.right)
|
||||
} else {
|
||||
walk_expr_for_fns(stmt.init)
|
||||
}
|
||||
}
|
||||
walk_expr_for_fns(stmt.test)
|
||||
walk_expr_for_fns(stmt.update)
|
||||
walk_stmts_for_fns(stmt.statements)
|
||||
} else if (k == "return" || k == "go") {
|
||||
walk_expr_for_fns(stmt.expression)
|
||||
} else if (k == "block") {
|
||||
walk_stmts_for_fns(stmt.statements)
|
||||
} else if (k == "label") {
|
||||
if (stmt.statement != null) walk_stmts_for_fns([stmt.statement])
|
||||
}
|
||||
i = i + 1
|
||||
}
|
||||
}
|
||||
|
||||
walk_stmts_for_fns(ast.statements)
|
||||
walk_stmts_for_fns(ast.functions)
|
||||
|
||||
// Update intrinsics: collect what's still referenced
|
||||
var used_intrinsics = {}
|
||||
var collect_intrinsics = null
|
||||
var collect_expr_intrinsics = null
|
||||
|
||||
collect_expr_intrinsics = function(expr) {
|
||||
if (expr == null) return null
|
||||
var k = expr.kind
|
||||
var i = 0
|
||||
if (k == "name" && expr.level == -1 && expr.name != null && expr.make != "functino") {
|
||||
used_intrinsics[expr.name] = true
|
||||
}
|
||||
if (expr.left != null) collect_expr_intrinsics(expr.left)
|
||||
if (expr.right != null) collect_expr_intrinsics(expr.right)
|
||||
if (expr.expression != null) collect_expr_intrinsics(expr.expression)
|
||||
if (expr.then != null) collect_expr_intrinsics(expr.then)
|
||||
if (expr.else != null) collect_expr_intrinsics(expr.else)
|
||||
if (k == "(" || k == "array" || k == "text literal") {
|
||||
i = 0
|
||||
while (i < length(expr.list)) {
|
||||
collect_expr_intrinsics(expr.list[i])
|
||||
i = i + 1
|
||||
}
|
||||
}
|
||||
if (k == "record") {
|
||||
i = 0
|
||||
while (i < length(expr.list)) {
|
||||
collect_expr_intrinsics(expr.list[i].right)
|
||||
i = i + 1
|
||||
}
|
||||
}
|
||||
if (k == "function") {
|
||||
collect_intrinsics(expr.statements)
|
||||
collect_intrinsics(expr.disruption)
|
||||
i = 0
|
||||
while (i < length(expr.list)) {
|
||||
if (expr.list[i].expression != null) {
|
||||
collect_expr_intrinsics(expr.list[i].expression)
|
||||
}
|
||||
i = i + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
collect_intrinsics = function(stmts) {
|
||||
if (stmts == null) return null
|
||||
var i = 0
|
||||
var j = 0
|
||||
var pi = 0
|
||||
var stmt = null
|
||||
var k = null
|
||||
while (i < length(stmts)) {
|
||||
stmt = stmts[i]
|
||||
k = stmt.kind
|
||||
if (k == "var" || k == "def") {
|
||||
collect_expr_intrinsics(stmt.right)
|
||||
} else if (k == "var_list") {
|
||||
j = 0
|
||||
while (j < length(stmt.list)) {
|
||||
collect_expr_intrinsics(stmt.list[j].right)
|
||||
j = j + 1
|
||||
}
|
||||
} else if (k == "call") {
|
||||
collect_expr_intrinsics(stmt.expression)
|
||||
} else if (k == "if") {
|
||||
collect_expr_intrinsics(stmt.expression)
|
||||
collect_intrinsics(stmt.then)
|
||||
collect_intrinsics(stmt.list)
|
||||
if (stmt.else != null) collect_intrinsics(stmt.else)
|
||||
} else if (k == "while" || k == "do") {
|
||||
collect_expr_intrinsics(stmt.expression)
|
||||
collect_intrinsics(stmt.statements)
|
||||
} else if (k == "for") {
|
||||
if (stmt.init != null) {
|
||||
if (stmt.init.kind == "var" || stmt.init.kind == "def") {
|
||||
collect_expr_intrinsics(stmt.init.right)
|
||||
} else {
|
||||
collect_expr_intrinsics(stmt.init)
|
||||
}
|
||||
}
|
||||
collect_expr_intrinsics(stmt.test)
|
||||
collect_expr_intrinsics(stmt.update)
|
||||
collect_intrinsics(stmt.statements)
|
||||
} else if (k == "return" || k == "go") {
|
||||
collect_expr_intrinsics(stmt.expression)
|
||||
} else if (k == "function") {
|
||||
collect_intrinsics(stmt.statements)
|
||||
collect_intrinsics(stmt.disruption)
|
||||
pi = 0
|
||||
while (pi < length(stmt.list)) {
|
||||
if (stmt.list[pi].expression != null) {
|
||||
collect_expr_intrinsics(stmt.list[pi].expression)
|
||||
}
|
||||
pi = pi + 1
|
||||
}
|
||||
} else if (k == "block") {
|
||||
collect_intrinsics(stmt.statements)
|
||||
} else if (k == "label") {
|
||||
if (stmt.statement != null) collect_intrinsics([stmt.statement])
|
||||
}
|
||||
i = i + 1
|
||||
}
|
||||
}
|
||||
|
||||
collect_intrinsics(ast.statements)
|
||||
collect_intrinsics(ast.functions)
|
||||
|
||||
var new_intrinsics = []
|
||||
i = 0
|
||||
while (i < length(ast.intrinsics)) {
|
||||
if (used_intrinsics[ast.intrinsics[i]] == true) {
|
||||
push(new_intrinsics, ast.intrinsics[i])
|
||||
}
|
||||
i = i + 1
|
||||
}
|
||||
ast.intrinsics = new_intrinsics
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Main
|
||||
// ============================================================
|
||||
|
||||
pre_scan()
|
||||
|
||||
// Pass 2: fold all statements and functions
|
||||
ast.statements = fold_stmts(ast.statements, 0)
|
||||
var fi = 0
|
||||
while (fi < length(ast.functions)) {
|
||||
fold_fn(ast.functions[fi])
|
||||
fi = fi + 1
|
||||
}
|
||||
|
||||
// Remove dead top-level functions
|
||||
var live_fns = []
|
||||
var fn = null
|
||||
fi = 0
|
||||
while (fi < length(ast.functions)) {
|
||||
fn = ast.functions[fi]
|
||||
if (fn.dead != true) {
|
||||
push(live_fns, fn)
|
||||
}
|
||||
fi = fi + 1
|
||||
}
|
||||
ast.functions = live_fns
|
||||
|
||||
// Pass 3: cleanup
|
||||
cleanup()
|
||||
|
||||
return ast
|
||||
}
|
||||
|
||||
return fold
|
||||
403
gc_plan.md
Normal file
403
gc_plan.md
Normal file
@@ -0,0 +1,403 @@
|
||||
# Plan: Complete Copying GC Implementation
|
||||
|
||||
## Overview
|
||||
|
||||
Remove reference counting (DupValue/FreeValue) entirely and complete the Cheney copying garbage collector. Each JSContext will use bump allocation from a heap block, and when out of memory, request a new heap from JSRuntime's buddy allocator and copy live objects to the new heap.
|
||||
|
||||
## Target Architecture (from docs/memory.md)
|
||||
|
||||
### Object Types (simplified from current):
|
||||
|
||||
**Type 0 - Array**: `{ header, length, elements[] }`
|
||||
**Type 1 - Blob**: `{ header, length, bits[] }`
|
||||
**Type 2 - Text**: `{ header, length_or_hash, packed_chars[] }`
|
||||
**Type 3 - Record**: `{ header, prototype, length, key_value_pairs[] }`
|
||||
**Type 4 - Function**: `{ header, code_ptr, outer_frame_ptr }` - 3 words only, always stone
|
||||
**Type 5 - Frame**: `{ header, function_ptr, caller_ptr, ret_addr, args[], closure_vars[], local_vars[], temps[] }`
|
||||
**Type 6 - Code**: Lives in immutable memory only, never copied
|
||||
**Type 7 - Forward**: Object has moved; cap56 contains new address
|
||||
|
||||
### Key Design Points:
|
||||
- **JSFunction** is just a pointer to code and a pointer to the frame that created it (3 words)
|
||||
- **Closure variables live in frames** - when a function returns, its frame is "reduced" to just the closure variables
|
||||
- **Code objects are immutable** - stored in stone memory, never copied during GC
|
||||
- **Frame reduction**: When a function returns, `caller` is set to zero, signaling the frame can be shrunk
|
||||
|
||||
## Current State (needs refactoring)
|
||||
|
||||
1. **Partial Cheney GC exists** at `source/quickjs.c:1844-2030`: `ctx_gc`, `gc_copy_value`, `gc_scan_object`
|
||||
2. **744 calls to JS_DupValue/JS_FreeValue** scattered throughout (currently undefined, causing compilation errors)
|
||||
3. **Current JSFunction** is bloated (has kind, name, union of cfunc/bytecode/bound) - needs simplification
|
||||
4. **Current JSVarRef** is a separate object - should be eliminated, closures live in frames
|
||||
5. **Bump allocator** in `js_malloc` (line 1495) with `heap_base`/`heap_free`/`heap_end`
|
||||
6. **Buddy allocator** for memory blocks (lines 1727-1837)
|
||||
7. **Header offset inconsistency** - some structs have header at offset 0, some at offset 8
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
### Phase 1: Define No-Op DupValue/FreeValue (To Enable Compilation)
|
||||
|
||||
Add these near line 100 in `source/quickjs.c`:
|
||||
|
||||
```c
|
||||
/* Copying GC - no reference counting needed */
|
||||
#define JS_DupValue(ctx, v) (v)
|
||||
#define JS_FreeValue(ctx, v) ((void)0)
|
||||
#define JS_DupValueRT(rt, v) (v)
|
||||
#define JS_FreeValueRT(rt, v) ((void)0)
|
||||
```
|
||||
|
||||
This makes the code compile while keeping existing call sites (they become no-ops).
|
||||
|
||||
### Phase 2: Standardize Object Headers (offset 0)
|
||||
|
||||
Remove `JSGCObjectHeader` (ref counting remnant) and put `objhdr_t` at offset 0:
|
||||
|
||||
```c
|
||||
typedef struct JSArray {
|
||||
objhdr_t hdr; // offset 0
|
||||
word_t length;
|
||||
JSValue values[];
|
||||
} JSArray;
|
||||
|
||||
typedef struct JSRecord {
|
||||
objhdr_t hdr; // offset 0
|
||||
JSRecord *proto;
|
||||
word_t length;
|
||||
slot slots[];
|
||||
} JSRecord;
|
||||
|
||||
typedef struct JSText {
|
||||
objhdr_t hdr; // offset 0
|
||||
word_t length; // pretext: length, text: hash
|
||||
word_t packed[];
|
||||
} JSText;
|
||||
|
||||
typedef struct JSBlob {
|
||||
objhdr_t hdr; // offset 0
|
||||
word_t length;
|
||||
uint8_t bits[];
|
||||
} JSBlob;
|
||||
|
||||
/* Simplified JSFunction per memory.md - 3 words */
|
||||
typedef struct JSFunction {
|
||||
objhdr_t hdr; // offset 0, always stone
|
||||
JSCode *code; // pointer to immutable code object
|
||||
struct JSFrame *outer; // frame that created this function
|
||||
} JSFunction;
|
||||
|
||||
/* JSFrame per memory.md */
|
||||
typedef struct JSFrame {
|
||||
objhdr_t hdr; // offset 0
|
||||
JSFunction *function; // function being executed
|
||||
struct JSFrame *caller; // calling frame (NULL = reduced/returned)
|
||||
word_t ret_addr; // return instruction address
|
||||
JSValue slots[]; // args, closure vars, locals, temps
|
||||
} JSFrame;
|
||||
|
||||
/* JSCode - always in immutable (stone) memory */
|
||||
typedef struct JSCode {
|
||||
objhdr_t hdr; // offset 0, always stone
|
||||
word_t arity; // max number of inputs
|
||||
word_t frame_size; // capacity of activation frame
|
||||
word_t closure_size; // reduced capacity for returned frames
|
||||
word_t entry_point; // address to begin execution
|
||||
word_t disruption_point;// address of disruption clause
|
||||
uint8_t bytecode[]; // actual bytecode
|
||||
} JSCode;
|
||||
```
|
||||
|
||||
### Phase 3: Complete gc_object_size for All Types
|
||||
|
||||
Update `gc_object_size` (line 1850) to read header at offset 0:
|
||||
|
||||
```c
|
||||
static size_t gc_object_size(void *ptr) {
|
||||
objhdr_t hdr = *(objhdr_t*)ptr; // Header at offset 0
|
||||
uint8_t type = objhdr_type(hdr);
|
||||
uint64_t cap = objhdr_cap56(hdr);
|
||||
|
||||
switch (type) {
|
||||
case OBJ_ARRAY:
|
||||
return sizeof(JSArray) + cap * sizeof(JSValue);
|
||||
case OBJ_BLOB:
|
||||
return sizeof(JSBlob) + (cap + 7) / 8; // cap is bits
|
||||
case OBJ_TEXT:
|
||||
return sizeof(JSText) + ((cap + 1) / 2) * sizeof(uint64_t);
|
||||
case OBJ_RECORD:
|
||||
return sizeof(JSRecord) + (cap + 1) * sizeof(slot); // cap is mask
|
||||
case OBJ_FUNCTION:
|
||||
return sizeof(JSFunction); // 3 words
|
||||
case OBJ_FRAME:
|
||||
return sizeof(JSFrame) + cap * sizeof(JSValue); // cap is slot count
|
||||
case OBJ_CODE:
|
||||
return 0; // Code is never copied (immutable)
|
||||
default:
|
||||
return 64; // Conservative fallback
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Phase 4: Complete gc_scan_object for All Types
|
||||
|
||||
Update `gc_scan_object` (line 1924):
|
||||
|
||||
```c
|
||||
static void gc_scan_object(JSContext *ctx, void *ptr, uint8_t **to_free, uint8_t *to_end) {
|
||||
objhdr_t hdr = *(objhdr_t*)ptr;
|
||||
uint8_t type = objhdr_type(hdr);
|
||||
|
||||
switch (type) {
|
||||
case OBJ_ARRAY: {
|
||||
JSArray *arr = (JSArray*)ptr;
|
||||
for (uint32_t i = 0; i < arr->length; i++) {
|
||||
arr->values[i] = gc_copy_value(ctx, arr->values[i], to_free, to_end);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case OBJ_RECORD: {
|
||||
JSRecord *rec = (JSRecord*)ptr;
|
||||
// Copy prototype
|
||||
if (rec->proto) {
|
||||
JSValue proto_val = JS_MKPTR(rec->proto);
|
||||
proto_val = gc_copy_value(ctx, proto_val, to_free, to_end);
|
||||
rec->proto = (JSRecord*)JS_VALUE_GET_PTR(proto_val);
|
||||
}
|
||||
// Copy table entries
|
||||
uint32_t mask = objhdr_cap56(rec->hdr);
|
||||
for (uint32_t i = 1; i <= mask; i++) { // Skip slot 0
|
||||
JSValue k = rec->slots[i].key;
|
||||
if (!rec_key_is_empty(k) && !rec_key_is_tomb(k)) {
|
||||
rec->slots[i].key = gc_copy_value(ctx, k, to_free, to_end);
|
||||
rec->slots[i].value = gc_copy_value(ctx, rec->slots[i].value, to_free, to_end);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case OBJ_FUNCTION: {
|
||||
JSFunction *func = (JSFunction*)ptr;
|
||||
// Code is immutable, don't copy - but outer frame needs copying
|
||||
if (func->outer) {
|
||||
JSValue outer_val = JS_MKPTR(func->outer);
|
||||
outer_val = gc_copy_value(ctx, outer_val, to_free, to_end);
|
||||
func->outer = (JSFrame*)JS_VALUE_GET_PTR(outer_val);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case OBJ_FRAME: {
|
||||
JSFrame *frame = (JSFrame*)ptr;
|
||||
// Copy function pointer
|
||||
if (frame->function) {
|
||||
JSValue func_val = JS_MKPTR(frame->function);
|
||||
func_val = gc_copy_value(ctx, func_val, to_free, to_end);
|
||||
frame->function = (JSFunction*)JS_VALUE_GET_PTR(func_val);
|
||||
}
|
||||
// Copy caller (unless NULL = reduced frame)
|
||||
if (frame->caller) {
|
||||
JSValue caller_val = JS_MKPTR(frame->caller);
|
||||
caller_val = gc_copy_value(ctx, caller_val, to_free, to_end);
|
||||
frame->caller = (JSFrame*)JS_VALUE_GET_PTR(caller_val);
|
||||
}
|
||||
// Copy all slots (args, closure vars, locals, temps)
|
||||
uint32_t slot_count = objhdr_cap56(frame->hdr);
|
||||
for (uint32_t i = 0; i < slot_count; i++) {
|
||||
frame->slots[i] = gc_copy_value(ctx, frame->slots[i], to_free, to_end);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case OBJ_TEXT:
|
||||
case OBJ_BLOB:
|
||||
case OBJ_CODE:
|
||||
// No internal references to scan
|
||||
break;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Phase 5: Fix gc_copy_value Forwarding
|
||||
|
||||
Update `gc_copy_value` (line 1883) for offset 0 headers:
|
||||
|
||||
```c
|
||||
static JSValue gc_copy_value(JSContext *ctx, JSValue v, uint8_t **to_free, uint8_t *to_end) {
|
||||
if (!JS_IsPtr(v)) return v; // Immediate value
|
||||
|
||||
void *ptr = JS_VALUE_GET_PTR(v);
|
||||
|
||||
// Stone memory - don't copy (includes Code objects)
|
||||
objhdr_t hdr = *(objhdr_t*)ptr;
|
||||
if (objhdr_s(hdr)) return v;
|
||||
|
||||
// Check if in current heap
|
||||
if ((uint8_t*)ptr < ctx->heap_base || (uint8_t*)ptr >= ctx->heap_end)
|
||||
return v; // External allocation
|
||||
|
||||
// Already forwarded?
|
||||
if (objhdr_type(hdr) == OBJ_FORWARD) {
|
||||
void *new_ptr = (void*)(uintptr_t)objhdr_cap56(hdr);
|
||||
return JS_MKPTR(new_ptr);
|
||||
}
|
||||
|
||||
// Copy object to new space
|
||||
size_t size = gc_object_size(ptr);
|
||||
void *new_ptr = *to_free;
|
||||
*to_free += size;
|
||||
memcpy(new_ptr, ptr, size);
|
||||
|
||||
// Leave forwarding pointer in old location
|
||||
*(objhdr_t*)ptr = objhdr_make((uint64_t)(uintptr_t)new_ptr, OBJ_FORWARD, 0, 0, 0, 0);
|
||||
|
||||
return JS_MKPTR(new_ptr);
|
||||
}
|
||||
```
|
||||
|
||||
### Phase 6: Complete GC Root Tracing
|
||||
|
||||
Update `ctx_gc` (line 1966) to trace all roots including JSGCRef:
|
||||
|
||||
```c
|
||||
static int ctx_gc(JSContext *ctx) {
|
||||
// ... existing setup code ...
|
||||
|
||||
// Copy roots: global object, class prototypes, etc. (existing)
|
||||
ctx->global_obj = gc_copy_value(ctx, ctx->global_obj, &to_free, to_end);
|
||||
ctx->global_var_obj = gc_copy_value(ctx, ctx->global_var_obj, &to_free, to_end);
|
||||
// ... other existing root copying ...
|
||||
|
||||
// Copy GC root stack (JS_PUSH_VALUE/JS_POP_VALUE)
|
||||
for (JSGCRef *ref = ctx->top_gc_ref; ref; ref = ref->prev) {
|
||||
ref->val = gc_copy_value(ctx, ref->val, &to_free, to_end);
|
||||
}
|
||||
|
||||
// Copy GC root list (JS_AddGCRef/JS_DeleteGCRef)
|
||||
for (JSGCRef *ref = ctx->last_gc_ref; ref; ref = ref->prev) {
|
||||
ref->val = gc_copy_value(ctx, ref->val, &to_free, to_end);
|
||||
}
|
||||
|
||||
// Copy current exception
|
||||
ctx->current_exception = gc_copy_value(ctx, ctx->current_exception, &to_free, to_end);
|
||||
|
||||
// Cheney scan (existing)
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### Phase 7: Trigger GC on Allocation Failure
|
||||
|
||||
Update `js_malloc` (line 1495):
|
||||
|
||||
```c
|
||||
void *js_malloc(JSContext *ctx, size_t size) {
|
||||
size = (size + 7) & ~7; // Align to 8 bytes
|
||||
|
||||
if ((uint8_t*)ctx->heap_free + size > (uint8_t*)ctx->heap_end) {
|
||||
if (ctx_gc(ctx) < 0) {
|
||||
JS_ThrowOutOfMemory(ctx);
|
||||
return NULL;
|
||||
}
|
||||
// Retry after GC
|
||||
if ((uint8_t*)ctx->heap_free + size > (uint8_t*)ctx->heap_end) {
|
||||
JS_ThrowOutOfMemory(ctx);
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
void *ptr = ctx->heap_free;
|
||||
ctx->heap_free = (uint8_t*)ctx->heap_free + size;
|
||||
return ptr;
|
||||
}
|
||||
```
|
||||
|
||||
### Phase 8: Frame Reduction (for closures)
|
||||
|
||||
When a function returns, "reduce" its frame to just closure variables:
|
||||
|
||||
```c
|
||||
static void reduce_frame(JSContext *ctx, JSFrame *frame) {
|
||||
if (frame->caller == NULL) return; // Already reduced
|
||||
|
||||
JSCode *code = frame->function->code;
|
||||
uint32_t closure_size = code->closure_size;
|
||||
|
||||
// Shrink capacity to just closure variables
|
||||
frame->hdr = objhdr_make(closure_size, OBJ_FRAME, 0, 0, 0, 0);
|
||||
frame->caller = NULL; // Signal: frame is reduced
|
||||
}
|
||||
```
|
||||
|
||||
### Phase 9: Remove Unused Reference Counting Code
|
||||
|
||||
Delete:
|
||||
- `gc_decref`, `gc_decref_child` functions
|
||||
- `gc_scan_incref_child`, `gc_scan_incref_child2` functions
|
||||
- `JS_GCPhaseEnum`, `gc_phase` fields
|
||||
- `JSGCObjectHeader` struct (merge into objhdr_t)
|
||||
- `ref_count` fields from any remaining structs
|
||||
- `mark_function_children_decref` function
|
||||
- All `free_*` functions that rely on ref counting
|
||||
|
||||
## Files to Modify
|
||||
|
||||
1. **source/quickjs.c** - Main implementation:
|
||||
- Add DupValue/FreeValue no-op macros (~line 100)
|
||||
- Restructure JSArray, JSBlob, JSText, JSRecord (lines 468-499)
|
||||
- Simplify JSFunction to 3-word struct (line 1205)
|
||||
- Add JSFrame as heap object (new)
|
||||
- Restructure JSCode/JSFunctionBytecode (line 1293)
|
||||
- Fix gc_object_size (line 1850)
|
||||
- Fix gc_copy_value (line 1883)
|
||||
- Complete gc_scan_object (line 1924)
|
||||
- Update ctx_gc for all roots (line 1966)
|
||||
- Update js_malloc to trigger GC (line 1495)
|
||||
- Delete ref counting code throughout
|
||||
|
||||
2. **source/quickjs.h** - Public API:
|
||||
- Remove JSGCObjectHeader
|
||||
- Update JSValue type checks if needed
|
||||
- Ensure JS_IsStone works with offset 0 headers
|
||||
|
||||
## Execution Order
|
||||
|
||||
1. **First**: Add DupValue/FreeValue macros (enables compilation)
|
||||
2. **Second**: Standardize struct layouts (header at offset 0)
|
||||
3. **Third**: Fix gc_object_size and gc_copy_value
|
||||
4. **Fourth**: Complete gc_scan_object for all types
|
||||
5. **Fifth**: Update ctx_gc with complete root tracing
|
||||
6. **Sixth**: Wire js_malloc to trigger GC
|
||||
7. **Seventh**: Add frame reduction for closures
|
||||
8. **Finally**: Remove ref counting dead code
|
||||
|
||||
## Verification
|
||||
|
||||
1. **Compile test**: `make` should succeed without errors
|
||||
2. **Basic test**: Run simple scripts:
|
||||
```js
|
||||
var a = [1, 2, 3]
|
||||
log.console(a[1])
|
||||
```
|
||||
3. **Stress test**: Allocate many objects to trigger GC:
|
||||
```js
|
||||
for (var i = 0; i < 100000; i++) {
|
||||
var x = { value: i }
|
||||
}
|
||||
log.console("done")
|
||||
```
|
||||
4. **Closure test**: Test functions with closures survive GC:
|
||||
```js
|
||||
fn make_counter() {
|
||||
var count = 0
|
||||
fn inc() { count = count + 1; return count }
|
||||
return inc
|
||||
}
|
||||
var c = make_counter()
|
||||
log.console(c()) // 1
|
||||
log.console(c()) // 2
|
||||
```
|
||||
5. **GC stress with closures**: Create many closures, trigger GC, verify they still work
|
||||
|
||||
## Key Design Decisions (Resolved)
|
||||
|
||||
1. **JSCode storage**: Lives in stone (immutable) memory, never copied during GC ✓
|
||||
2. **Header offset**: Standardized to offset 0 for all heap objects ✓
|
||||
3. **Closure variables**: Live in JSFrame objects; frames are "reduced" when functions return ✓
|
||||
4. **JSVarRef**: Eliminated - closures reference their outer frame directly ✓
|
||||
236
graph.ce
Normal file
236
graph.ce
Normal file
@@ -0,0 +1,236 @@
|
||||
// cell graph [<locator>] - Emit dependency graph
|
||||
//
|
||||
// Usage:
|
||||
// cell graph Graph current directory package
|
||||
// cell graph . Graph current directory package
|
||||
// cell graph <locator> Graph specific package
|
||||
// cell graph --world Graph all packages in shop (world set)
|
||||
//
|
||||
// Options:
|
||||
// --format <fmt> Output format: tree (default), dot, json
|
||||
// --resolved Show resolved view with links applied (default)
|
||||
// --locked Show lock view without links
|
||||
// --world Graph all packages in shop
|
||||
|
||||
var shop = use('internal/shop')
|
||||
var pkg = use('package')
|
||||
var link = use('link')
|
||||
var fd = use('fd')
|
||||
var json = use('json')
|
||||
|
||||
var target_locator = null
|
||||
var format = 'tree'
|
||||
var show_locked = false
|
||||
var show_world = false
|
||||
|
||||
for (var i = 0; i < length(args); i++) {
|
||||
if (args[i] == '--format' || args[i] == '-f') {
|
||||
if (i + 1 < length(args)) {
|
||||
format = args[++i]
|
||||
if (format != 'tree' && format != 'dot' && format != 'json') {
|
||||
log.error('Invalid format: ' + format + '. Must be tree, dot, or json')
|
||||
$stop()
|
||||
}
|
||||
} else {
|
||||
log.error('--format requires a format type')
|
||||
$stop()
|
||||
}
|
||||
} else if (args[i] == '--resolved') {
|
||||
show_locked = false
|
||||
} else if (args[i] == '--locked') {
|
||||
show_locked = true
|
||||
} else if (args[i] == '--world') {
|
||||
show_world = true
|
||||
} else if (args[i] == '--help' || args[i] == '-h') {
|
||||
log.console("Usage: cell graph [<locator>] [options]")
|
||||
log.console("")
|
||||
log.console("Emit the dependency graph.")
|
||||
log.console("")
|
||||
log.console("Options:")
|
||||
log.console(" --format <fmt> Output format: tree (default), dot, json")
|
||||
log.console(" --resolved Show resolved view with links applied (default)")
|
||||
log.console(" --locked Show lock view without links")
|
||||
log.console(" --world Graph all packages in shop")
|
||||
$stop()
|
||||
} else if (!starts_with(args[i], '-')) {
|
||||
target_locator = args[i]
|
||||
}
|
||||
}
|
||||
|
||||
var links = show_locked ? {} : link.load()
|
||||
|
||||
// Get effective locator (after links)
|
||||
function get_effective(locator) {
|
||||
return links[locator] || locator
|
||||
}
|
||||
|
||||
// Build graph data structure
|
||||
var nodes = {}
|
||||
var edges = []
|
||||
|
||||
function add_node(locator) {
|
||||
if (nodes[locator]) return
|
||||
|
||||
var lock = shop.load_lock()
|
||||
var lock_entry = lock[locator]
|
||||
var link_target = links[locator]
|
||||
var info = shop.resolve_package_info(locator)
|
||||
|
||||
nodes[locator] = {
|
||||
id: locator,
|
||||
effective: get_effective(locator),
|
||||
linked: link_target != null,
|
||||
local: info == 'local',
|
||||
commit: lock_entry && lock_entry.commit ? text(lock_entry.commit, 0, 8) : null
|
||||
}
|
||||
}
|
||||
|
||||
function gather_graph(locator, visited) {
|
||||
if (visited[locator]) return
|
||||
visited[locator] = true
|
||||
|
||||
add_node(locator)
|
||||
|
||||
try {
|
||||
var deps = pkg.dependencies(locator)
|
||||
if (deps) {
|
||||
arrfor(array(deps), function(alias) {
|
||||
var dep_locator = deps[alias]
|
||||
add_node(dep_locator)
|
||||
push(edges, { from: locator, to: dep_locator, alias: alias })
|
||||
gather_graph(dep_locator, visited)
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
// Package might not have dependencies
|
||||
}
|
||||
}
|
||||
|
||||
// Gather graph from roots
|
||||
var roots = []
|
||||
|
||||
if (show_world) {
|
||||
// Use all packages in shop as roots
|
||||
var packages = shop.list_packages()
|
||||
arrfor(packages, function(p) {
|
||||
if (p != 'core') {
|
||||
push(roots, p)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
// Default to current directory
|
||||
if (!target_locator) {
|
||||
target_locator = '.'
|
||||
}
|
||||
|
||||
// Resolve local paths
|
||||
if (target_locator == '.' || starts_with(target_locator, './') || starts_with(target_locator, '../') || fd.is_dir(target_locator)) {
|
||||
var resolved = fd.realpath(target_locator)
|
||||
if (resolved) {
|
||||
target_locator = resolved
|
||||
}
|
||||
}
|
||||
|
||||
push(roots, target_locator)
|
||||
}
|
||||
|
||||
arrfor(roots, function(root) {
|
||||
gather_graph(root, {})
|
||||
})
|
||||
|
||||
// Output based on format
|
||||
if (format == 'tree') {
|
||||
function print_tree(locator, prefix, is_last, visited) {
|
||||
if (visited[locator]) {
|
||||
log.console(prefix + (is_last ? "\\-- " : "|-- ") + locator + " (circular)")
|
||||
return
|
||||
}
|
||||
visited[locator] = true
|
||||
|
||||
var node = nodes[locator]
|
||||
var suffix = ""
|
||||
if (node.linked) suffix += " -> " + node.effective
|
||||
if (node.commit) suffix += " @" + node.commit
|
||||
if (node.local) suffix += " (local)"
|
||||
|
||||
log.console(prefix + (is_last ? "\\-- " : "|-- ") + locator + suffix)
|
||||
|
||||
// Get children
|
||||
var children = []
|
||||
arrfor(edges, function(e) {
|
||||
if (e.from == locator) {
|
||||
push(children, e)
|
||||
}
|
||||
})
|
||||
|
||||
for (var i = 0; i < length(children); i++) {
|
||||
var child_prefix = prefix + (is_last ? " " : "| ")
|
||||
print_tree(children[i].to, child_prefix, i == length(children) - 1, visited)
|
||||
}
|
||||
}
|
||||
|
||||
for (var i = 0; i < length(roots); i++) {
|
||||
log.console(roots[i])
|
||||
|
||||
var children = []
|
||||
arrfor(edges, function(e) {
|
||||
if (e.from == roots[i]) {
|
||||
push(children, e)
|
||||
}
|
||||
})
|
||||
|
||||
for (var j = 0; j < length(children); j++) {
|
||||
print_tree(children[j].to, "", j == length(children) - 1, {})
|
||||
}
|
||||
|
||||
if (i < length(roots) - 1) log.console("")
|
||||
}
|
||||
|
||||
} else if (format == 'dot') {
|
||||
log.console("digraph dependencies {")
|
||||
log.console(" rankdir=TB;")
|
||||
log.console(" node [shape=box];")
|
||||
log.console("")
|
||||
|
||||
// Node definitions
|
||||
arrfor(array(nodes), function(id) {
|
||||
var node = nodes[id]
|
||||
var label = id
|
||||
if (node.commit) label += "\\n@" + node.commit
|
||||
var attrs = 'label="' + label + '"'
|
||||
if (node.linked) attrs += ', style=dashed'
|
||||
if (node.local) attrs += ', color=blue'
|
||||
|
||||
// Safe node ID for dot
|
||||
var safe_id = replace(id, /[^a-zA-Z0-9]/g, '_')
|
||||
log.console(' ' + safe_id + ' [' + attrs + '];')
|
||||
})
|
||||
|
||||
log.console("")
|
||||
|
||||
// Edges
|
||||
arrfor(edges, function(e) {
|
||||
var from_id = replace(e.from, /[^a-zA-Z0-9]/g, '_')
|
||||
var to_id = replace(e.to, /[^a-zA-Z0-9]/g, '_')
|
||||
var label = e.alias != e.to ? 'label="' + e.alias + '"' : ''
|
||||
log.console(' ' + from_id + ' -> ' + to_id + (label ? ' [' + label + ']' : '') + ';')
|
||||
})
|
||||
|
||||
log.console("}")
|
||||
|
||||
} else if (format == 'json') {
|
||||
var output = {
|
||||
nodes: [],
|
||||
edges: []
|
||||
}
|
||||
|
||||
arrfor(array(nodes), function(id) {
|
||||
push(output.nodes, nodes[id])
|
||||
})
|
||||
|
||||
output.edges = edges
|
||||
|
||||
log.console(json.encode(output))
|
||||
}
|
||||
|
||||
$stop()
|
||||
46
help.ce
46
help.ce
@@ -2,7 +2,7 @@
|
||||
|
||||
var fd = use('fd')
|
||||
|
||||
var command = args.length > 0 ? args[0] : null
|
||||
var command = length(args) > 0 ? args[0] : null
|
||||
|
||||
// Display specific command help
|
||||
if (command) {
|
||||
@@ -27,21 +27,41 @@ if (stat && stat.isFile) {
|
||||
log.console(content)
|
||||
} else {
|
||||
// Fallback if man file doesn't exist
|
||||
log.console("cell - The Cell module system for Prosperon")
|
||||
log.console("cell - The Cell package manager")
|
||||
log.console("")
|
||||
log.console("Usage: cell <command> [arguments]")
|
||||
log.console("")
|
||||
log.console("Commands:")
|
||||
log.console(" init Initialize a new Cell project")
|
||||
log.console(" get Fetch and add a module dependency")
|
||||
log.console(" update Update a dependency to a new version")
|
||||
log.console(" vendor Copy all dependencies locally")
|
||||
log.console(" build Compile all modules to bytecode")
|
||||
log.console(" patch Create a patch for a module")
|
||||
log.console(" config Manage system and actor configurations")
|
||||
log.console(" help Show this help message")
|
||||
log.console("Package Management:")
|
||||
log.console(" install <locator> Install a package and its dependencies")
|
||||
log.console(" update [locator] Update packages from remote sources")
|
||||
log.console(" remove <locator> Remove a package from the shop")
|
||||
log.console(" add <locator> Add a dependency to current package")
|
||||
log.console("")
|
||||
log.console("Run 'cell help <command>' for more information on a command.")
|
||||
log.console("Building:")
|
||||
log.console(" build [locator] Build dynamic libraries for packages")
|
||||
log.console(" clean [scope] Remove build artifacts")
|
||||
log.console("")
|
||||
log.console("Linking (Local Development):")
|
||||
log.console(" link <origin> <target> Link a package to a local path")
|
||||
log.console(" unlink <origin> Remove a package link")
|
||||
log.console(" clone <origin> <path> Clone and link a package locally")
|
||||
log.console("")
|
||||
log.console("Information:")
|
||||
log.console(" list [scope] List packages and dependencies")
|
||||
log.console(" ls [locator] List modules and actors in a package")
|
||||
log.console(" why <locator> Show reverse dependencies")
|
||||
log.console(" search <query> Search for packages, modules, or actors")
|
||||
log.console("")
|
||||
log.console("Diagnostics:")
|
||||
log.console(" resolve [locator] Print fully resolved dependency closure")
|
||||
log.console(" graph [locator] Emit dependency graph (tree, dot, json)")
|
||||
log.console(" verify [scope] Verify integrity and consistency")
|
||||
log.console("")
|
||||
log.console("Other:")
|
||||
log.console(" help [command] Show help for a command")
|
||||
log.console(" version Show cell version")
|
||||
log.console("")
|
||||
log.console("Run 'cell <command> --help' for more information on a command.")
|
||||
}
|
||||
|
||||
$stop()
|
||||
$stop()
|
||||
|
||||
177
install.ce
177
install.ce
@@ -1,62 +1,185 @@
|
||||
// cell install <locator> - Install a package to the shop
|
||||
// Does not modify the current project's cell.toml
|
||||
//
|
||||
// Usage:
|
||||
// cell install <locator> Install a package and its dependencies
|
||||
// cell install . Install current directory package
|
||||
//
|
||||
// Options:
|
||||
// --target <triple> Build for target platform
|
||||
// --refresh Refresh floating refs before locking
|
||||
// --dry-run Show what would be installed
|
||||
|
||||
var shop = use('internal/shop')
|
||||
var build = use('build')
|
||||
var pkg = use('package')
|
||||
var fd = use('fd')
|
||||
|
||||
if (args.length < 1) {
|
||||
log.console("Usage: cell install <locator>")
|
||||
if (length(args) < 1) {
|
||||
log.console("Usage: cell install <locator> [options]")
|
||||
log.console("")
|
||||
log.console("Options:")
|
||||
log.console(" --target <triple> Build for target platform")
|
||||
log.console(" --refresh Refresh floating refs before locking")
|
||||
log.console(" --dry-run Show what would be installed")
|
||||
$stop()
|
||||
return
|
||||
}
|
||||
|
||||
var locator = args[0]
|
||||
var locator = null
|
||||
var target_triple = null
|
||||
var refresh = false
|
||||
var dry_run = false
|
||||
|
||||
for (var i = 0; i < length(args); i++) {
|
||||
if (args[i] == '--target' || args[i] == '-t') {
|
||||
if (i + 1 < length(args)) {
|
||||
target_triple = args[++i]
|
||||
} else {
|
||||
log.error('--target requires a triple')
|
||||
$stop()
|
||||
}
|
||||
} else if (args[i] == '--refresh') {
|
||||
refresh = true
|
||||
} else if (args[i] == '--dry-run') {
|
||||
dry_run = true
|
||||
} else if (args[i] == '--help' || args[i] == '-h') {
|
||||
log.console("Usage: cell install <locator> [options]")
|
||||
log.console("")
|
||||
log.console("Install a package and its dependencies to the shop.")
|
||||
log.console("")
|
||||
log.console("Options:")
|
||||
log.console(" --target <triple> Build for target platform")
|
||||
log.console(" --refresh Refresh floating refs before locking")
|
||||
log.console(" --dry-run Show what would be installed")
|
||||
$stop()
|
||||
} else if (!starts_with(args[i], '-')) {
|
||||
locator = args[i]
|
||||
}
|
||||
}
|
||||
|
||||
if (!locator) {
|
||||
log.console("Usage: cell install <locator>")
|
||||
$stop()
|
||||
}
|
||||
|
||||
// Resolve relative paths to absolute paths
|
||||
// Local paths like '.' or '../foo' need to be converted to absolute paths
|
||||
if (locator == '.' || locator.startsWith('./') || locator.startsWith('../') || fd.is_dir(locator)) {
|
||||
if (locator == '.' || starts_with(locator, './') || starts_with(locator, '../') || fd.is_dir(locator)) {
|
||||
var resolved = fd.realpath(locator)
|
||||
if (resolved) {
|
||||
locator = resolved
|
||||
}
|
||||
}
|
||||
|
||||
// Default target
|
||||
if (!target_triple) {
|
||||
target_triple = build.detect_host_target()
|
||||
}
|
||||
|
||||
log.console("Installing " + locator + "...")
|
||||
|
||||
var pkg = use('package')
|
||||
// Gather all packages that will be installed
|
||||
var packages_to_install = []
|
||||
var skipped_packages = []
|
||||
var visited = {}
|
||||
|
||||
// Recursive install function that handles dependencies
|
||||
function install_package(pkg_locator, visited) {
|
||||
function gather_packages(pkg_locator) {
|
||||
if (visited[pkg_locator]) return
|
||||
visited[pkg_locator] = true
|
||||
|
||||
// First, add to lock.toml
|
||||
shop.update(pkg_locator)
|
||||
|
||||
// Extract/symlink the package so we can read its cell.toml
|
||||
shop.extract(pkg_locator)
|
||||
|
||||
// Now get direct dependencies and install them first
|
||||
|
||||
// Check if this is a local path that doesn't exist
|
||||
if (starts_with(pkg_locator, '/') && !fd.is_dir(pkg_locator)) {
|
||||
push(skipped_packages, pkg_locator)
|
||||
log.console(" Skipping missing local package: " + pkg_locator)
|
||||
return
|
||||
}
|
||||
|
||||
push(packages_to_install, pkg_locator)
|
||||
|
||||
// Try to read dependencies
|
||||
try {
|
||||
// For packages not yet extracted, we need to update and extract first to read deps
|
||||
var lock = shop.load_lock()
|
||||
if (!lock[pkg_locator]) {
|
||||
if (!dry_run) {
|
||||
var update_result = shop.update(pkg_locator)
|
||||
if (update_result) {
|
||||
shop.extract(pkg_locator)
|
||||
} else {
|
||||
// Update failed - package might not be fetchable
|
||||
log.console("Warning: Could not fetch " + pkg_locator)
|
||||
return
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Package is in lock, ensure it's extracted
|
||||
if (!dry_run) {
|
||||
shop.extract(pkg_locator)
|
||||
}
|
||||
}
|
||||
|
||||
var deps = pkg.dependencies(pkg_locator)
|
||||
if (deps) {
|
||||
for (var alias in deps) {
|
||||
arrfor(array(deps), function(alias) {
|
||||
var dep_locator = deps[alias]
|
||||
log.console("Installing dependency " + dep_locator)
|
||||
install_package(dep_locator, visited)
|
||||
}
|
||||
gather_packages(dep_locator)
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
// Package might not have dependencies or cell.toml issue
|
||||
log.console("Warning: Could not read dependencies for " + pkg_locator + ": " + e.message)
|
||||
if (!dry_run) {
|
||||
log.console(`Warning: Could not read dependencies for ${pkg_locator}: ${e.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
// Build the package after all dependencies are installed
|
||||
build.build_package(pkg_locator)
|
||||
}
|
||||
|
||||
install_package(locator, {})
|
||||
log.console("Installed " + locator)
|
||||
// Gather all packages
|
||||
gather_packages(locator)
|
||||
|
||||
if (dry_run) {
|
||||
log.console("Would install:")
|
||||
arrfor(packages_to_install, function(p) {
|
||||
var lock = shop.load_lock()
|
||||
var exists = lock[p] != null
|
||||
log.console(" " + p + (exists ? " (already installed)" : ""))
|
||||
})
|
||||
if (length(skipped_packages) > 0) {
|
||||
log.console("")
|
||||
log.console("Would skip (missing local paths):")
|
||||
arrfor(skipped_packages, function(p) {
|
||||
log.console(" " + p)
|
||||
})
|
||||
}
|
||||
$stop()
|
||||
}
|
||||
|
||||
// Install each package
|
||||
function install_package(pkg_locator) {
|
||||
// Update lock entry
|
||||
shop.update(pkg_locator)
|
||||
|
||||
// Extract/symlink the package
|
||||
shop.extract(pkg_locator)
|
||||
|
||||
// Build scripts
|
||||
shop.build_package_scripts(pkg_locator)
|
||||
|
||||
// Build C code
|
||||
try {
|
||||
build.build_dynamic(pkg_locator, target_triple, 'release')
|
||||
} catch (e) {
|
||||
// Not all packages have C code
|
||||
}
|
||||
}
|
||||
|
||||
arrfor(packages_to_install, function(p) {
|
||||
log.console(" Installing " + p + "...")
|
||||
install_package(p)
|
||||
})
|
||||
|
||||
var summary = "Installed " + text(length(packages_to_install)) + " package(s)."
|
||||
if (length(skipped_packages) > 0) {
|
||||
summary += " Skipped " + text(length(skipped_packages)) + " missing local path(s)."
|
||||
}
|
||||
log.console(summary)
|
||||
|
||||
$stop()
|
||||
|
||||
137
internal/bootstrap.cm
Normal file
137
internal/bootstrap.cm
Normal file
@@ -0,0 +1,137 @@
|
||||
// Hidden vars (os, args, core_path, use_mcode) come from env
|
||||
// args[0] = script name, args[1..] = user args
|
||||
var load_internal = os.load_internal
|
||||
function use_embed(name) {
|
||||
return load_internal("js_" + name + "_use")
|
||||
}
|
||||
|
||||
var fd = use_embed('fd')
|
||||
var json = use_embed('json')
|
||||
|
||||
var use_cache = {}
|
||||
use_cache['fd'] = fd
|
||||
use_cache['os'] = os
|
||||
use_cache['json'] = json
|
||||
|
||||
// Bootstrap: load tokenize.cm, parse.cm, fold.cm from pre-compiled mach bytecode
|
||||
function use_basic(path) {
|
||||
if (use_cache[path])
|
||||
return use_cache[path]
|
||||
var result = use_embed(replace(path, '/', '_'))
|
||||
use_cache[path] = result
|
||||
return result
|
||||
}
|
||||
|
||||
// Load a module from .mach bytecode, falling back to .ast.json
|
||||
function boot_load(name, env) {
|
||||
var mach_path = name + ".mach"
|
||||
var data = null
|
||||
if (fd.is_file(mach_path)) {
|
||||
data = fd.slurp(mach_path)
|
||||
return mach_load(data, env)
|
||||
}
|
||||
data = text(fd.slurp(name + ".ast.json"))
|
||||
return mach_eval_ast(name, data, env)
|
||||
}
|
||||
|
||||
var boot_env = {use: use_basic}
|
||||
var tokenize_mod = boot_load("tokenize", boot_env)
|
||||
var parse_mod = boot_load("parse", boot_env)
|
||||
var fold_mod = boot_load("fold", boot_env)
|
||||
|
||||
// Optionally load mcode compiler module
|
||||
var mcode_mod = null
|
||||
if (use_mcode) {
|
||||
mcode_mod = boot_load("mcode", boot_env)
|
||||
}
|
||||
|
||||
// analyze: tokenize + parse, check for errors
|
||||
function analyze(src, filename) {
|
||||
var tok_result = tokenize_mod(src, filename)
|
||||
var ast = parse_mod(tok_result.tokens, src, filename, tokenize_mod)
|
||||
var _i = 0
|
||||
var prev_line = -1
|
||||
var prev_msg = null
|
||||
var e = null
|
||||
var msg = null
|
||||
var line = null
|
||||
var col = null
|
||||
var has_errors = ast.errors != null && length(ast.errors) > 0
|
||||
if (has_errors) {
|
||||
while (_i < length(ast.errors)) {
|
||||
e = ast.errors[_i]
|
||||
msg = e.message
|
||||
line = e.line
|
||||
col = e.column
|
||||
if (msg != prev_msg || line != prev_line) {
|
||||
if (line != null && col != null) {
|
||||
print(`${filename}:${text(line)}:${text(col)}: error: ${msg}`)
|
||||
} else {
|
||||
print(`${filename}: error: ${msg}`)
|
||||
}
|
||||
}
|
||||
prev_line = line
|
||||
prev_msg = msg
|
||||
_i = _i + 1
|
||||
}
|
||||
disrupt
|
||||
}
|
||||
ast = fold_mod(ast)
|
||||
return ast
|
||||
}
|
||||
|
||||
// Run AST through either mcode or mach pipeline
|
||||
function run_ast(name, ast, env) {
|
||||
var compiled = null
|
||||
if (use_mcode) {
|
||||
compiled = mcode_mod(ast)
|
||||
return mcode_run(name, json.encode(compiled), env)
|
||||
}
|
||||
return mach_eval_ast(name, json.encode(ast), env)
|
||||
}
|
||||
|
||||
// use() with ƿit pipeline for .cm modules
|
||||
function use(path) {
|
||||
var file_path = path + '.cm'
|
||||
var script = null
|
||||
var ast = null
|
||||
var result = null
|
||||
if (use_cache[path])
|
||||
return use_cache[path]
|
||||
|
||||
// Check CWD first, then core_path
|
||||
if (!fd.is_file(file_path))
|
||||
file_path = core_path + '/' + path + '.cm'
|
||||
|
||||
if (fd.is_file(file_path)) {
|
||||
script = text(fd.slurp(file_path))
|
||||
ast = analyze(script, file_path)
|
||||
result = run_ast(path, ast, {use: use})
|
||||
use_cache[path] = result
|
||||
return result
|
||||
}
|
||||
|
||||
// Fallback to embedded C module
|
||||
result = use_embed(replace(path, '/', '_'))
|
||||
use_cache[path] = result
|
||||
return result
|
||||
}
|
||||
|
||||
// Load and run the user's program
|
||||
var program = args[0]
|
||||
var script_file = program
|
||||
|
||||
// Add .ce extension if not already present
|
||||
if (!ends_with(script_file, '.ce') && !ends_with(script_file, '.cm'))
|
||||
script_file = program + '.ce'
|
||||
|
||||
var user_args = []
|
||||
var _j = 1
|
||||
while (_j < length(args)) {
|
||||
push(user_args, args[_j])
|
||||
_j = _j + 1
|
||||
}
|
||||
|
||||
var script = text(fd.slurp(script_file))
|
||||
var ast = analyze(script, script_file)
|
||||
run_ast(program, ast, {use: use, args: user_args, json: json})
|
||||
BIN
internal/bootstrap.mach
Normal file
BIN
internal/bootstrap.mach
Normal file
Binary file not shown.
@@ -1,35 +1,31 @@
|
||||
(function engine() {
|
||||
var _cell = globalThis.cell
|
||||
delete globalThis.cell
|
||||
var ACTORDATA = _cell.hidden.actorsym
|
||||
// Hidden vars (os, actorsym, init, core_path) come from env
|
||||
var ACTORDATA = actorsym
|
||||
var SYSYM = '__SYSTEM__'
|
||||
|
||||
var hidden = _cell.hidden
|
||||
|
||||
var os = hidden.os;
|
||||
|
||||
_cell.os = null
|
||||
var _cell = {}
|
||||
var need_stop = false
|
||||
|
||||
var dylib_ext
|
||||
|
||||
_cell.id ??= "newguy"
|
||||
var cases = {
|
||||
Windows: '.dll',
|
||||
macOS: '.dylib',
|
||||
Linux: '.so'
|
||||
}
|
||||
|
||||
switch(os.platform()) {
|
||||
case 'Windows': dylib_ext = '.dll'; break;
|
||||
case 'macOS': dylib_ext = '.dylib'; break;
|
||||
case 'Linux': dylib_ext = '.so'; break;
|
||||
}
|
||||
print(os.platform())
|
||||
|
||||
dylib_ext = cases[os.platform()]
|
||||
|
||||
var MOD_EXT = '.cm'
|
||||
var ACTOR_EXT = '.ce'
|
||||
|
||||
var load_internal = os.load_internal
|
||||
function use_embed(name) {
|
||||
return load_internal(`js_${name}_use`)
|
||||
return load_internal("js_" + name + "_use")
|
||||
}
|
||||
|
||||
globalThis.logical = function(val1)
|
||||
{
|
||||
function logical(val1) {
|
||||
if (val1 == 0 || val1 == false || val1 == "false" || val1 == null)
|
||||
return false;
|
||||
if (val1 == 1 || val1 == true || val1 == "true")
|
||||
@@ -37,20 +33,38 @@ globalThis.logical = function(val1)
|
||||
return null;
|
||||
}
|
||||
|
||||
function some(arr, pred) {
|
||||
return find(arr, pred) != null
|
||||
}
|
||||
|
||||
function every(arr, pred) {
|
||||
return find(arr, x => not(pred(x))) == null
|
||||
}
|
||||
|
||||
function starts_with(str, prefix) {
|
||||
return search(str, prefix) == 0
|
||||
}
|
||||
|
||||
function ends_with(str, suffix) {
|
||||
return search(str, suffix, -length(suffix)) != null
|
||||
}
|
||||
|
||||
var js = use_embed('js')
|
||||
var fd = use_embed('fd')
|
||||
|
||||
// Get the shop path from HOME environment
|
||||
var home = os.getenv('HOME') || os.getenv('USERPROFILE')
|
||||
if (!home) {
|
||||
throw new Error('Could not determine home directory')
|
||||
os.print('Could not determine home directory\n')
|
||||
os.exit(1)
|
||||
}
|
||||
var shop_path = home + '/.cell'
|
||||
var packages_path = shop_path + '/packages'
|
||||
var core_path = packages_path + '/core'
|
||||
|
||||
if (!fd.is_dir(core_path)) {
|
||||
throw new Error('Cell shop not found at ' + shop_path + '. Run "cell install" to set up.')
|
||||
os.print('Cell shop not found at ' + shop_path + '. Run "cell install" to set up.\n')
|
||||
os.exit(1)
|
||||
}
|
||||
|
||||
var use_cache = {}
|
||||
@@ -62,7 +76,7 @@ function use_core(path) {
|
||||
if (use_cache[cache_key])
|
||||
return use_cache[cache_key];
|
||||
|
||||
var sym = use_embed(path.replace('/','_'))
|
||||
var sym = use_embed(replace(path, '/', '_'))
|
||||
|
||||
// Core scripts are in packages/core/
|
||||
var file_path = core_path + '/' + path + MOD_EXT
|
||||
@@ -71,8 +85,8 @@ function use_core(path) {
|
||||
var script_blob = fd.slurp(file_path)
|
||||
var script = text(script_blob)
|
||||
var mod = `(function setup_module(use){${script}})`
|
||||
var fn = js.eval('core:' + path, mod)
|
||||
var result = fn.call(sym, use_core);
|
||||
var fn = mach_eval('core:' + path, mod)
|
||||
var result = call(fn,sym, [use_core])
|
||||
use_cache[cache_key] = result;
|
||||
return result;
|
||||
}
|
||||
@@ -83,14 +97,7 @@ function use_core(path) {
|
||||
|
||||
var blob = use_core('blob')
|
||||
|
||||
// Capture Object and Array methods before they're deleted
|
||||
Object.prototype.toString = function()
|
||||
{
|
||||
return json.encode(this)
|
||||
}
|
||||
|
||||
globalThis.actor = function()
|
||||
{
|
||||
function actor() {
|
||||
|
||||
}
|
||||
|
||||
@@ -98,43 +105,8 @@ var actor_mod = use_core('actor')
|
||||
var wota = use_core('wota')
|
||||
var nota = use_core('nota')
|
||||
|
||||
globalThis.isa = function(value, master) {
|
||||
if (master == null) return false
|
||||
|
||||
// isa(value, function) - check if function.prototype is in chain
|
||||
if (typeof master == 'function') {
|
||||
// Special type checks
|
||||
if (master == stone) return is_stone(value)
|
||||
if (master == number) return is_number(value)
|
||||
if (master == text) return is_text(value)
|
||||
if (master == logical) return is_logical(value)
|
||||
if (master == array) return is_array(value)
|
||||
if (master == object) return is_object(value)
|
||||
if (master == fn) return is_function(value)
|
||||
if (master == actor) return is_object(value) && value[ACTORDATA]
|
||||
|
||||
// Check prototype chain
|
||||
if (master.prototype) {
|
||||
var proto = _getPrototypeOf(value)
|
||||
while (proto != null) {
|
||||
if (proto == master.prototype) return true
|
||||
proto = _getPrototypeOf(proto)
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// isa(object, master_object) - check prototype chain
|
||||
if (typeof master == 'object') {
|
||||
var proto = _getPrototypeOf(value)
|
||||
while (proto != null) {
|
||||
if (proto == master) return true
|
||||
proto = _getPrototypeOf(proto)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
return false
|
||||
function is_actor(value) {
|
||||
return is_object(value) && value[ACTORDATA]
|
||||
}
|
||||
|
||||
var ENETSERVICE = 0.1
|
||||
@@ -145,12 +117,12 @@ function caller_data(depth = 0)
|
||||
var file = "nofile"
|
||||
var line = 0
|
||||
|
||||
var caller = new Error().stack.split("\n")[1+depth]
|
||||
var caller = array(Error().stack, "\n")[1+depth]
|
||||
if (caller) {
|
||||
var md = caller.match(/\((.*)\:/)
|
||||
var md = extract(caller, /\((.*)\:/)
|
||||
var m = md ? md[1] : "SCRIPT"
|
||||
if (m) file = m
|
||||
md = caller.match(/\:(\d*)\)/)
|
||||
md = extract(caller, /\:(\d*)\)/)
|
||||
m = md ? md[1] : 0
|
||||
if (m) line = m
|
||||
}
|
||||
@@ -159,64 +131,70 @@ function caller_data(depth = 0)
|
||||
}
|
||||
|
||||
function console_rec(line, file, msg) {
|
||||
return `[${_cell.id.slice(0,5)}] [${file}:${line}]: ${msg}\n`
|
||||
return `[${text(_cell.id, 0, 5)}] [${file}:${line}]: ${msg}\n`
|
||||
// time: [${time.text("mb d yyyy h:nn:ss")}]
|
||||
}
|
||||
|
||||
globalThis.log = {}
|
||||
log.console = function(msg)
|
||||
{
|
||||
function log(name, args) {
|
||||
var caller = caller_data(1)
|
||||
os.print(console_rec(caller.line, caller.file, msg))
|
||||
var msg = args[0]
|
||||
|
||||
if (name == 'console') {
|
||||
os.print(console_rec(caller.line, caller.file, msg))
|
||||
} else if (name == 'error') {
|
||||
if (msg == null) msg = Error()
|
||||
if (is_proto(msg, Error))
|
||||
msg = msg.name + ": " + msg.message + "\n" + msg.stack
|
||||
os.print(console_rec(caller.line, caller.file, msg))
|
||||
} else if (name == 'system') {
|
||||
msg = "[SYSTEM] " + msg
|
||||
os.print(console_rec(caller.line, caller.file, msg))
|
||||
} else {
|
||||
log.console(`unknown log type: ${name}`)
|
||||
}
|
||||
}
|
||||
|
||||
log.error = function(msg = new Error())
|
||||
function actor_die(err)
|
||||
{
|
||||
var caller = caller_data(1)
|
||||
|
||||
if (msg instanceof Error)
|
||||
msg = msg.name + ": " + msg.message + "\n" + msg.stack
|
||||
|
||||
os.print(console_rec(caller.line,caller.file,msg))
|
||||
}
|
||||
if (err && is_function(err.toString)) {
|
||||
os.print(err.toString())
|
||||
os.print("\n")
|
||||
if (err.stack) os.print(err.stack)
|
||||
}
|
||||
|
||||
log.system = function(msg) {
|
||||
msg = "[SYSTEM] " + msg
|
||||
log.console(msg)
|
||||
}
|
||||
|
||||
function disrupt(err)
|
||||
{
|
||||
if (overling) {
|
||||
if (err) {
|
||||
// with an err, this is a forceful disrupt
|
||||
var reason = (err instanceof Error) ? err.stack : err
|
||||
var reason = (is_proto(err, Error)) ? err.stack : err
|
||||
report_to_overling({type:'disrupt', reason})
|
||||
} else
|
||||
report_to_overling({type:'stop'})
|
||||
}
|
||||
|
||||
if (underlings) {
|
||||
for (var id of underlings) {
|
||||
var unders = array(underlings)
|
||||
arrfor(unders, function(id, index) {
|
||||
log.console(`calling on ${id} to disrupt too`)
|
||||
$_.stop(create_actor({id}))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
if (err) {
|
||||
log.console(err);
|
||||
if (err.message)
|
||||
log.console(err.message)
|
||||
if (err.stack)
|
||||
log.console(err.stack)
|
||||
}
|
||||
|
||||
actor_mod.disrupt()
|
||||
actor_mod["disrupt"]()
|
||||
}
|
||||
|
||||
actor_mod.on_exception(disrupt)
|
||||
|
||||
_cell.args = _cell.hidden.init
|
||||
_cell.args ??= {}
|
||||
_cell.id ??= "newguy"
|
||||
|
||||
actor_mod.on_exception(actor_die)
|
||||
|
||||
_cell.args = init != null ? init : {}
|
||||
_cell.id = "newguy"
|
||||
|
||||
function create_actor(desc = {id:guid()}) {
|
||||
var actor = {}
|
||||
@@ -237,18 +215,42 @@ var json = use_core('json')
|
||||
var time = use_core('time')
|
||||
|
||||
var pronto = use_core('pronto')
|
||||
globalThis.fallback = pronto.fallback
|
||||
globalThis.parallel = pronto.parallel
|
||||
globalThis.race = pronto.race
|
||||
globalThis.sequence = pronto.sequence
|
||||
var fallback = pronto.fallback
|
||||
var parallel = pronto.parallel
|
||||
var race = pronto.race
|
||||
var sequence = pronto.sequence
|
||||
|
||||
// Create runtime environment for modules
|
||||
var runtime_env = {
|
||||
logical: logical,
|
||||
some: some,
|
||||
every: every,
|
||||
starts_with: starts_with,
|
||||
ends_with: ends_with,
|
||||
actor: actor,
|
||||
is_actor: is_actor,
|
||||
log: log,
|
||||
send: send,
|
||||
fallback: fallback,
|
||||
parallel: parallel,
|
||||
race: race,
|
||||
sequence: sequence
|
||||
}
|
||||
|
||||
// Pass to os for shop to access
|
||||
os.runtime_env = runtime_env
|
||||
|
||||
$_.time_limit = function(requestor, seconds)
|
||||
{
|
||||
if (!pronto.is_requestor(requestor))
|
||||
throw new Error('time_limit: first argument must be a requestor');
|
||||
if (!isa(seconds, number) || seconds <= 0)
|
||||
throw new Error('time_limit: seconds must be a positive number');
|
||||
|
||||
if (!pronto.is_requestor(requestor)) {
|
||||
log.error('time_limit: first argument must be a requestor')
|
||||
disrupt
|
||||
}
|
||||
if (!is_number(seconds) || seconds <= 0) {
|
||||
log.error('time_limit: seconds must be a positive number')
|
||||
disrupt
|
||||
}
|
||||
|
||||
return function time_limit_requestor(callback, value) {
|
||||
pronto.check_callback(callback, 'time_limit')
|
||||
var finished = false
|
||||
@@ -263,7 +265,14 @@ $_.time_limit = function(requestor, seconds)
|
||||
timer_cancel = null
|
||||
}
|
||||
if (requestor_cancel) {
|
||||
try { pronto.requestor_cancel(reason) } catch (_) {}
|
||||
requestor_cancel(reason)
|
||||
requestor_cancel = null
|
||||
}
|
||||
}
|
||||
|
||||
function safe_cancel_requestor(reason) {
|
||||
if (requestor_cancel) {
|
||||
requestor_cancel(reason)
|
||||
requestor_cancel = null
|
||||
}
|
||||
}
|
||||
@@ -271,15 +280,12 @@ $_.time_limit = function(requestor, seconds)
|
||||
timer_cancel = $_.delay(function() {
|
||||
if (finished) return
|
||||
def reason = make_reason(factory, 'Timeout.', seconds)
|
||||
if (requestor_cancel) {
|
||||
try { requestor_cancel(reason) } catch (_) {}
|
||||
requestor_cancel = null
|
||||
}
|
||||
safe_cancel_requestor(reason)
|
||||
finished = true
|
||||
callback(null, reason)
|
||||
}, seconds)
|
||||
|
||||
try {
|
||||
function do_request() {
|
||||
requestor_cancel = requestor(function(val, reason) {
|
||||
if (finished) return
|
||||
finished = true
|
||||
@@ -289,16 +295,14 @@ $_.time_limit = function(requestor, seconds)
|
||||
}
|
||||
callback(val, reason)
|
||||
}, value)
|
||||
} catch (ex) {
|
||||
cancel(ex)
|
||||
callback(null, ex)
|
||||
} disruption {
|
||||
cancel(Error('requestor failed'))
|
||||
callback(null, Error('requestor failed'))
|
||||
}
|
||||
do_request()
|
||||
|
||||
return function(reason) {
|
||||
if (requestor_cancel) {
|
||||
try { requestor_cancel(reason) } catch (_) {}
|
||||
requestor_cancel = null
|
||||
}
|
||||
safe_cancel_requestor(reason)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -343,12 +347,12 @@ REPLYTIMEOUT = config.reply_timeout
|
||||
|
||||
function guid(bits = 256)
|
||||
{
|
||||
var guid = new blob(bits, os.random)
|
||||
var guid = blob(bits, os.random)
|
||||
stone(guid)
|
||||
return text(guid,'h')
|
||||
}
|
||||
|
||||
var HEADER = key()
|
||||
var HEADER = {}
|
||||
|
||||
// takes a function input value that will eventually be called with the current time in number form.
|
||||
$_.clock = function(fn) {
|
||||
@@ -358,7 +362,7 @@ $_.clock = function(fn) {
|
||||
})
|
||||
}
|
||||
|
||||
var underlings = new Set() // this is more like "all actors that are notified when we die"
|
||||
var underlings = {} // this is more like "all actors that are notified when we die"
|
||||
var overling = null
|
||||
var root = null
|
||||
|
||||
@@ -411,51 +415,54 @@ var portal_fn = null
|
||||
|
||||
// takes a function input value that will eventually be called with the current time in number form.
|
||||
$_.portal = function(fn, port) {
|
||||
if (portal) throw new Error(`Already started a portal listening on ${portal.port}`)
|
||||
if (!port) throw new Error("Requires a valid port.")
|
||||
if (portal) {
|
||||
log.error(`Already started a portal listening on ${portal.port}`)
|
||||
disrupt
|
||||
}
|
||||
if (!port) {
|
||||
log.error("Requires a valid port.")
|
||||
disrupt
|
||||
}
|
||||
log.system(`starting a portal on port ${port}`)
|
||||
portal = enet.create_host({address: "any", port})
|
||||
portal_fn = fn
|
||||
}
|
||||
|
||||
function handle_host(e) {
|
||||
switch (e.type) {
|
||||
case "connect":
|
||||
log.system(`connected a new peer: ${e.peer.address}:${e.peer.port}`)
|
||||
peers[`${e.peer.address}:${e.peer.port}`] = e.peer
|
||||
var queue = peer_queue.get(e.peer)
|
||||
if (queue) {
|
||||
for (var msg of queue) e.peer.send(nota.encode(msg))
|
||||
log.system(`sent ${msg} out of queue`)
|
||||
peer_queue.delete(e.peer)
|
||||
}
|
||||
break
|
||||
case "disconnect":
|
||||
if (e.type == "connect") {
|
||||
log.system(`connected a new peer: ${e.peer.address}:${e.peer.port}`)
|
||||
peers[`${e.peer.address}:${e.peer.port}`] = e.peer
|
||||
var queue = peer_queue.get(e.peer)
|
||||
if (queue) {
|
||||
arrfor(queue, (msg, index) => e.peer.send(nota.encode(msg)))
|
||||
log.system(`sent ${msg} out of queue`)
|
||||
peer_queue.delete(e.peer)
|
||||
for (var id in peers) if (peers[id] == e.peer) delete peers[id]
|
||||
log.system('portal got disconnect from ' + e.peer.address + ":" + e.peer.port)
|
||||
break
|
||||
case "receive":
|
||||
var data = nota.decode(e.data)
|
||||
if (data.replycc && !data.replycc.address) {
|
||||
data.replycc[ACTORDATA].address = e.peer.address
|
||||
data.replycc[ACTORDATA].port = e.peer.port
|
||||
}
|
||||
} else if (e.type == "disconnect") {
|
||||
peer_queue.delete(e.peer)
|
||||
arrfor(array(peers), function(id, index) {
|
||||
if (peers[id] == e.peer) delete peers[id]
|
||||
})
|
||||
log.system('portal got disconnect from ' + e.peer.address + ":" + e.peer.port)
|
||||
} else if (e.type == "receive") {
|
||||
var data = nota.decode(e.data)
|
||||
if (data.replycc && !data.replycc.address) {
|
||||
data.replycc[ACTORDATA].address = e.peer.address
|
||||
data.replycc[ACTORDATA].port = e.peer.port
|
||||
}
|
||||
function populate_actor_addresses(obj) {
|
||||
if (!is_object(obj)) return
|
||||
if (obj[ACTORDATA] && !obj[ACTORDATA].address) {
|
||||
obj[ACTORDATA].address = e.peer.address
|
||||
obj[ACTORDATA].port = e.peer.port
|
||||
}
|
||||
function populate_actor_addresses(obj) {
|
||||
if (!isa(obj, object)) return
|
||||
if (obj[ACTORDATA] && !obj[ACTORDATA].address) {
|
||||
obj[ACTORDATA].address = e.peer.address
|
||||
obj[ACTORDATA].port = e.peer.port
|
||||
}
|
||||
for (var key in obj) {
|
||||
if (object.has(obj, key)) {
|
||||
populate_actor_addresses(obj[key])
|
||||
}
|
||||
}
|
||||
}
|
||||
if (data.data) populate_actor_addresses(data.data)
|
||||
turn(data)
|
||||
break
|
||||
arrfor(array(obj), function(key, index) {
|
||||
if (key in obj)
|
||||
populate_actor_addresses(obj[key])
|
||||
})
|
||||
}
|
||||
if (data.data) populate_actor_addresses(data.data)
|
||||
turn(data)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -469,20 +476,18 @@ $_.receiver = function receiver(fn) {
|
||||
receive_fn = fn
|
||||
}
|
||||
|
||||
$_.start = function start(cb, program, ...args) {
|
||||
$_.start = function start(cb, program) {
|
||||
if (!program) return
|
||||
|
||||
var id = guid()
|
||||
if (args.length == 1 && Array.isArray(args[0])) args = args[0]
|
||||
var startup = {
|
||||
id,
|
||||
overling: $_.self,
|
||||
root,
|
||||
arg: args,
|
||||
program,
|
||||
}
|
||||
greeters[id] = cb
|
||||
message_queue.push({ startup })
|
||||
push(message_queue, { startup })
|
||||
}
|
||||
|
||||
// stops an underling or self.
|
||||
@@ -491,10 +496,14 @@ $_.stop = function stop(actor) {
|
||||
need_stop = true
|
||||
return
|
||||
}
|
||||
if (!isa(actor, actor))
|
||||
throw new Error('Can only call stop on an actor.')
|
||||
if (!underlings.has(actor[ACTORDATA].id))
|
||||
throw new Error('Can only call stop on an underling or self.')
|
||||
if (!is_actor(actor)) {
|
||||
log.error('Can only call stop on an actor.')
|
||||
disrupt
|
||||
}
|
||||
if (is_null(underlings[actor[ACTORDATA].id])) {
|
||||
log.error('Can only call stop on an underling or self.')
|
||||
disrupt
|
||||
}
|
||||
|
||||
sys_msg(actor, {kind:"stop"})
|
||||
}
|
||||
@@ -506,11 +515,6 @@ $_.unneeded = function unneeded(fn, seconds) {
|
||||
|
||||
// schedules the invocation of a function after a specified amount of time.
|
||||
$_.delay = function delay(fn, seconds = 0) {
|
||||
if (seconds <= 0) {
|
||||
$_.clock(fn)
|
||||
return
|
||||
}
|
||||
|
||||
function delay_turn() {
|
||||
fn()
|
||||
send_messages()
|
||||
@@ -522,34 +526,36 @@ $_.delay = function delay(fn, seconds = 0) {
|
||||
var enet = use_core('enet')
|
||||
|
||||
// causes this actor to stop when another actor stops.
|
||||
var couplings = new Set()
|
||||
var couplings = {}
|
||||
$_.couple = function couple(actor) {
|
||||
if (actor == $_.self) return // can't couple to self
|
||||
couplings.add(actor[ACTORDATA].id)
|
||||
couplings[actor[ACTORDATA].id] = true
|
||||
sys_msg(actor, {kind:'couple', from: $_.self})
|
||||
log.system(`coupled to ${actor}`)
|
||||
}
|
||||
|
||||
function actor_prep(actor, send) {
|
||||
message_queue.push({actor,send});
|
||||
push(message_queue, {actor,send});
|
||||
}
|
||||
|
||||
// Send a message immediately without queuing
|
||||
function actor_send_immediate(actor, send) {
|
||||
try {
|
||||
actor_send(actor, send);
|
||||
} catch (err) {
|
||||
log.error("Failed to send immediate message:", err);
|
||||
}
|
||||
actor_send(actor, send)
|
||||
}
|
||||
|
||||
function actor_send(actor, message) {
|
||||
if (actor[HEADER] && !actor[HEADER].replycc) // attempting to respond to a message but sender is not expecting; silently drop
|
||||
return
|
||||
|
||||
if (!isa(actor, actor) && !isa(actor.replycc, actor)) throw new Error(`Must send to an actor object. Attempted send to ${actor}`)
|
||||
|
||||
if (typeof message != 'object') throw new Error('Must send an object record.')
|
||||
if (!is_actor(actor) && !is_actor(actor.replycc)) {
|
||||
log.error(`Must send to an actor object. Attempted send to ${actor}`)
|
||||
disrupt
|
||||
}
|
||||
|
||||
if (!is_object(message)) {
|
||||
log.error('Must send an object record.')
|
||||
disrupt
|
||||
}
|
||||
|
||||
// message to self
|
||||
if (actor[ACTORDATA].id == _cell.id) {
|
||||
@@ -560,7 +566,7 @@ function actor_send(actor, message) {
|
||||
// message to actor in same flock
|
||||
if (actor[ACTORDATA].id && actor_mod.mailbox_exist(actor[ACTORDATA].id)) {
|
||||
var wota_blob = wota.encode(message)
|
||||
// log.console(`sending wota blob of ${wota_blob.length/8} bytes`)
|
||||
// log.console(`sending wota blob of ${length(wota_blob)/8} bytes`)
|
||||
actor_mod.mailbox_push(actor[ACTORDATA].id, wota_blob)
|
||||
return
|
||||
}
|
||||
@@ -592,45 +598,50 @@ function actor_send(actor, message) {
|
||||
// Holds all messages queued during the current turn.
|
||||
var message_queue = []
|
||||
|
||||
var need_stop = false
|
||||
|
||||
function send_messages() {
|
||||
function send_messages() {
|
||||
// if we've been flagged to stop, bail out before doing anything
|
||||
if (need_stop) {
|
||||
disrupt()
|
||||
message_queue.length = 0
|
||||
actor_die()
|
||||
message_queue = []
|
||||
return
|
||||
}
|
||||
|
||||
for (var msg of message_queue) {
|
||||
arrfor(message_queue, function(msg, index) {
|
||||
if (msg.startup) {
|
||||
// now is the time to actually spin up the actor
|
||||
actor_mod.createactor(msg.startup)
|
||||
} else {
|
||||
actor_send(msg.actor, msg.send)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
message_queue.length = 0
|
||||
message_queue = []
|
||||
}
|
||||
|
||||
var replies = {}
|
||||
|
||||
globalThis.send = function send(actor, message, reply) {
|
||||
if (typeof actor != 'object')
|
||||
throw new Error(`Must send to an actor object. Provided: ${actor}`);
|
||||
|
||||
if (typeof message != 'object')
|
||||
throw new Error('Message must be an object')
|
||||
var send = {type:"user", data: message}
|
||||
function send(actor, message, reply) {
|
||||
if (!is_object(actor)) {
|
||||
log.error(`Must send to an actor object. Provided: ${actor}`)
|
||||
disrupt
|
||||
}
|
||||
|
||||
if (!is_object(message)) {
|
||||
log.error('Message must be an object')
|
||||
disrupt
|
||||
}
|
||||
var send_msg = {type:"user", data: message}
|
||||
var target = actor
|
||||
|
||||
if (actor[HEADER] && actor[HEADER].replycc) {
|
||||
var header = actor[HEADER]
|
||||
if (!header.replycc || !isa(header.replycc, actor))
|
||||
throw new Error(`Supplied actor had a return, but it's not a valid actor! ${actor[HEADER]}`)
|
||||
if (!header.replycc || !is_actor(header.replycc)) {
|
||||
log.error(`Supplied actor had a return, but it's not a valid actor! ${actor[HEADER]}`)
|
||||
disrupt
|
||||
}
|
||||
|
||||
actor = header.replycc
|
||||
send.return = header.reply
|
||||
target = header.replycc
|
||||
send_msg.return = header.reply
|
||||
}
|
||||
|
||||
if (reply) {
|
||||
@@ -642,12 +653,12 @@ globalThis.send = function send(actor, message, reply) {
|
||||
delete replies[id]
|
||||
}
|
||||
}, REPLYTIMEOUT)
|
||||
send.reply = id
|
||||
send.replycc = $_.self
|
||||
send_msg.reply = id
|
||||
send_msg.replycc = $_.self
|
||||
}
|
||||
|
||||
// Instead of sending immediately, queue it
|
||||
actor_prep(actor,send);
|
||||
actor_prep(target, send_msg);
|
||||
}
|
||||
|
||||
stone(send)
|
||||
@@ -666,7 +677,6 @@ function turn(msg)
|
||||
}
|
||||
|
||||
//log.console(`FIXME: need to get main from config, not just set to true`)
|
||||
//log.console(`FIXME: add freeze/unfreeze at this level, so we can do it (but scripts cannot)`)
|
||||
actor_mod.register_actor(_cell.id, turn, true, config.ar_timer)
|
||||
|
||||
if (config.actor_memory)
|
||||
@@ -679,7 +689,7 @@ overling = _cell.args.overling
|
||||
$_.overling = overling
|
||||
|
||||
root = _cell.args.root
|
||||
root ??= $_.self
|
||||
if (root == null) root = $_.self
|
||||
|
||||
if (overling) {
|
||||
$_.couple(overling) // auto couple to overling
|
||||
@@ -715,36 +725,35 @@ function handle_actor_disconnect(id) {
|
||||
delete greeters[id]
|
||||
}
|
||||
log.system(`actor ${id} disconnected`)
|
||||
if (couplings.has(id)) disrupt("coupled actor died") // couplings now disrupts instead of stop
|
||||
if (!is_null(couplings[id])) actor_die("coupled actor died") // couplings now disrupts instead of stop
|
||||
}
|
||||
|
||||
function handle_sysym(msg)
|
||||
{
|
||||
var from
|
||||
switch(msg.kind) {
|
||||
case 'stop':
|
||||
disrupt("got stop message")
|
||||
break
|
||||
case 'underling':
|
||||
from = msg.from
|
||||
var greeter = greeters[from[ACTORDATA].id]
|
||||
if (greeter) greeter(msg.message)
|
||||
if (msg.message.type == 'disrupt')
|
||||
underlings.delete(from[ACTORDATA].id)
|
||||
break
|
||||
case 'contact':
|
||||
if (portal_fn) {
|
||||
var letter2 = msg.data
|
||||
letter2[HEADER] = msg
|
||||
delete msg.data
|
||||
portal_fn(letter2)
|
||||
} else throw new Error('Got a contact message, but no portal is established.')
|
||||
break
|
||||
case 'couple': // from must be notified when we die
|
||||
from = msg.from
|
||||
underlings.add(from[ACTORDATA].id)
|
||||
log.system(`actor ${from} is coupled to me`)
|
||||
break
|
||||
if (msg.kind == 'stop') {
|
||||
actor_die("got stop message")
|
||||
} else if (msg.kind == 'underling') {
|
||||
from = msg.from
|
||||
var greeter = greeters[from[ACTORDATA].id]
|
||||
if (greeter) greeter(msg.message)
|
||||
if (msg.message.type == 'disrupt')
|
||||
delete underlings[from[ACTORDATA].id]
|
||||
} else if (msg.kind == 'contact') {
|
||||
if (portal_fn) {
|
||||
var letter2 = msg.data
|
||||
letter2[HEADER] = msg
|
||||
delete msg.data
|
||||
portal_fn(letter2)
|
||||
} else {
|
||||
log.error('Got a contact message, but no portal is established.')
|
||||
disrupt
|
||||
}
|
||||
} else if (msg.kind == 'couple') {
|
||||
// from must be notified when we die
|
||||
from = msg.from
|
||||
underlings[from[ACTORDATA].id] = true
|
||||
log.system(`actor ${from} is coupled to me`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -754,30 +763,27 @@ function handle_message(msg) {
|
||||
return
|
||||
}
|
||||
|
||||
switch (msg.type) {
|
||||
case "user":
|
||||
var letter = msg.data // what the sender really sent
|
||||
_ObjectDefineProperty(letter, HEADER, {
|
||||
value: msg, enumerable: false
|
||||
})
|
||||
_ObjectDefineProperty(letter, ACTORDATA, { // this is so is_actor == true
|
||||
value: { reply: msg.reply }, enumerable: false
|
||||
})
|
||||
|
||||
if (msg.return) {
|
||||
var fn = replies[msg.return]
|
||||
if (fn) fn(letter)
|
||||
delete replies[msg.return]
|
||||
return
|
||||
}
|
||||
|
||||
if (receive_fn) receive_fn(letter)
|
||||
if (msg.type == "user") {
|
||||
var letter = msg.data // what the sender really sent
|
||||
_ObjectDefineProperty(letter, HEADER, {
|
||||
value: msg, enumerable: false
|
||||
})
|
||||
_ObjectDefineProperty(letter, ACTORDATA, { // this is so is_actor == true
|
||||
value: { reply: msg.reply }, enumerable: false
|
||||
})
|
||||
|
||||
if (msg.return) {
|
||||
var fn = replies[msg.return]
|
||||
if (fn) fn(letter)
|
||||
delete replies[msg.return]
|
||||
return
|
||||
case "stopped":
|
||||
handle_actor_disconnect(msg.id)
|
||||
break
|
||||
}
|
||||
|
||||
if (receive_fn) receive_fn(letter)
|
||||
} else if (msg.type == "stopped") {
|
||||
handle_actor_disconnect(msg.id)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function enet_check()
|
||||
{
|
||||
@@ -802,38 +808,34 @@ if (!locator) {
|
||||
locator = shop.resolve_locator(_cell.args.program + ".ce", pkg)
|
||||
}
|
||||
|
||||
if (!locator)
|
||||
throw new Error(`Main program ${_cell.args.program} could not be found`)
|
||||
|
||||
stone(globalThis)
|
||||
|
||||
var rads = use_core("math/radians")
|
||||
log.console(rads)
|
||||
if (!locator) {
|
||||
os.print(`Main program ${_cell.args.program} could not be found\n`)
|
||||
os.exit(1)
|
||||
}
|
||||
|
||||
$_.clock(_ => {
|
||||
// Get capabilities for the main program
|
||||
var file_info = shop.file_info ? shop.file_info(locator.path) : null
|
||||
var inject = shop.script_inject_for ? shop.script_inject_for(file_info) : []
|
||||
|
||||
// Build values array for injection
|
||||
var vals = []
|
||||
for (var i = 0; i < inject.length; i++) {
|
||||
// Build env object for injection
|
||||
var env = {}
|
||||
for (var i = 0; i < length(inject); i++) {
|
||||
var key = inject[i]
|
||||
if (key && key[0] == '$') key = key.substring(1)
|
||||
if (key == 'fd') vals.push(fd)
|
||||
else vals.push($_[key])
|
||||
if (key && key[0] == '$') key = text(key, 1)
|
||||
if (key == 'fd') env[key] = fd
|
||||
else env[key] = $_[key]
|
||||
}
|
||||
|
||||
// Create use function bound to the program's package
|
||||
var pkg = file_info ? file_info.package : null
|
||||
var use_fn = function(path) { return shop.use(path, pkg) }
|
||||
|
||||
// Call with signature: setup_module(args, use, ...capabilities)
|
||||
// The script wrapper builds $_ from the injected capabilities for backward compatibility
|
||||
var val = locator.symbol.call(null, _cell.args.arg, use_fn, ...vals)
|
||||
// Call with signature: setup_module(args, use, env)
|
||||
// The script wrapper binds $delay, $start, etc. from env
|
||||
var val = call(locator.symbol, null, [_cell.args.arg, use_fn, env])
|
||||
|
||||
if (val)
|
||||
throw new Error('Program must not return anything');
|
||||
log.error('Program must not return anything')
|
||||
disrupt
|
||||
})
|
||||
|
||||
})()
|
||||
@@ -1,63 +0,0 @@
|
||||
#include "cell.h"
|
||||
|
||||
static JSValue js_json_encode(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) {
|
||||
if (argc < 1) return JS_ThrowTypeError(ctx, "json.encode requires at least 1 argument");
|
||||
|
||||
JSValue global = JS_GetGlobalObject(ctx);
|
||||
JSValue json = JS_GetPropertyStr(ctx, global, "JSON");
|
||||
JSValue stringify = JS_GetPropertyStr(ctx, json, "stringify");
|
||||
|
||||
JSValue args[3];
|
||||
args[0] = argv[0]; // value
|
||||
args[1] = (argc > 1) ? argv[1] : JS_NULL; // replacer
|
||||
args[2] = (argc > 2) ? argv[2] : JS_NewInt32(ctx, 1); // space, default 1
|
||||
|
||||
JSValue result = JS_Call(ctx, stringify, json, 3, args);
|
||||
|
||||
JS_FreeValue(ctx, stringify);
|
||||
JS_FreeValue(ctx, json);
|
||||
JS_FreeValue(ctx, global);
|
||||
|
||||
if (argc <= 2) JS_FreeValue(ctx, args[2]);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
static JSValue js_json_decode(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) {
|
||||
if (argc < 1) return JS_ThrowTypeError(ctx, "json.decode requires at least 1 argument");
|
||||
|
||||
if (!JS_IsString(argv[0])) {
|
||||
JSValue err = JS_NewError(ctx);
|
||||
JS_DefinePropertyValueStr(ctx, err, "message",
|
||||
JS_NewString(ctx, "couldn't parse text: not a string"),
|
||||
JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE);
|
||||
return JS_Throw(ctx, err);
|
||||
}
|
||||
|
||||
JSValue global = JS_GetGlobalObject(ctx);
|
||||
JSValue json = JS_GetPropertyStr(ctx, global, "JSON");
|
||||
JSValue parse = JS_GetPropertyStr(ctx, json, "parse");
|
||||
|
||||
JSValue args[2];
|
||||
args[0] = argv[0]; // text
|
||||
args[1] = (argc > 1) ? argv[1] : JS_NULL; // reviver
|
||||
|
||||
JSValue result = JS_Call(ctx, parse, json, argc > 1 ? 2 : 1, args);
|
||||
|
||||
JS_FreeValue(ctx, parse);
|
||||
JS_FreeValue(ctx, json);
|
||||
JS_FreeValue(ctx, global);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
static const JSCFunctionListEntry js_json_funcs[] = {
|
||||
JS_CFUNC_DEF("encode", 1, js_json_encode),
|
||||
JS_CFUNC_DEF("decode", 1, js_json_decode),
|
||||
};
|
||||
|
||||
JSValue js_json_use(JSContext *js) {
|
||||
JSValue export = JS_NewObject(js);
|
||||
JS_SetPropertyFunctionList(js, export, js_json_funcs, sizeof(js_json_funcs)/sizeof(JSCFunctionListEntry));
|
||||
return export;
|
||||
}
|
||||
385
internal/nota.c
385
internal/nota.c
@@ -1,385 +0,0 @@
|
||||
#include "cell.h"
|
||||
#include "cell_internal.h"
|
||||
|
||||
#define NOTA_IMPLEMENTATION
|
||||
#include "nota.h"
|
||||
|
||||
typedef struct NotaEncodeContext {
|
||||
JSContext *ctx;
|
||||
JSValue visitedStack;
|
||||
NotaBuffer nb;
|
||||
int cycle;
|
||||
JSValue replacer;
|
||||
} NotaEncodeContext;
|
||||
|
||||
static void nota_stack_push(NotaEncodeContext *enc, JSValueConst val)
|
||||
{
|
||||
JSContext *ctx = enc->ctx;
|
||||
int len = JS_ArrayLength(ctx, enc->visitedStack);
|
||||
JS_SetPropertyInt64(ctx, enc->visitedStack, len, JS_DupValue(ctx, val));
|
||||
}
|
||||
|
||||
static void nota_stack_pop(NotaEncodeContext *enc)
|
||||
{
|
||||
JSContext *ctx = enc->ctx;
|
||||
int len = JS_ArrayLength(ctx, enc->visitedStack);
|
||||
JS_SetPropertyStr(ctx, enc->visitedStack, "length", JS_NewUint32(ctx, len - 1));
|
||||
}
|
||||
|
||||
static int nota_stack_has(NotaEncodeContext *enc, JSValueConst val)
|
||||
{
|
||||
JSContext *ctx = enc->ctx;
|
||||
int len = JS_ArrayLength(ctx, enc->visitedStack);
|
||||
for (int i = 0; i < len; i++) {
|
||||
JSValue elem = JS_GetPropertyUint32(ctx, enc->visitedStack, i);
|
||||
if (JS_IsObject(elem) && JS_IsObject(val)) {
|
||||
if (JS_StrictEq(ctx, elem, val)) {
|
||||
JS_FreeValue(ctx, elem);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
JS_FreeValue(ctx, elem);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static JSValue apply_replacer(NotaEncodeContext *enc, JSValueConst holder, JSValueConst key, JSValueConst val) {
|
||||
if (JS_IsNull(enc->replacer)) return JS_DupValue(enc->ctx, val);
|
||||
|
||||
JSValue args[2] = { JS_DupValue(enc->ctx, key), JS_DupValue(enc->ctx, val) };
|
||||
JSValue result = JS_Call(enc->ctx, enc->replacer, holder, 2, args);
|
||||
JS_FreeValue(enc->ctx, args[0]);
|
||||
JS_FreeValue(enc->ctx, args[1]);
|
||||
|
||||
if (JS_IsException(result)) return JS_DupValue(enc->ctx, val);
|
||||
return result;
|
||||
}
|
||||
|
||||
char *js_do_nota_decode(JSContext *js, JSValue *tmp, char *nota, JSValue holder, JSValue key, JSValue reviver) {
|
||||
int type = nota_type(nota);
|
||||
JSValue ret2;
|
||||
long long n;
|
||||
double d;
|
||||
int b;
|
||||
char *str;
|
||||
uint8_t *blob;
|
||||
|
||||
switch(type) {
|
||||
case NOTA_BLOB:
|
||||
nota = nota_read_blob(&n, (char**)&blob, nota);
|
||||
*tmp = js_new_blob_stoned_copy(js, blob, n);
|
||||
free(blob);
|
||||
break;
|
||||
case NOTA_TEXT:
|
||||
nota = nota_read_text(&str, nota);
|
||||
*tmp = JS_NewString(js, str);
|
||||
free(str);
|
||||
break;
|
||||
case NOTA_ARR:
|
||||
nota = nota_read_array(&n, nota);
|
||||
*tmp = JS_NewArray(js);
|
||||
for (int i = 0; i < n; i++) {
|
||||
nota = js_do_nota_decode(js, &ret2, nota, *tmp, JS_NewInt32(js, i), reviver);
|
||||
JS_SetPropertyInt64(js, *tmp, i, ret2);
|
||||
}
|
||||
break;
|
||||
case NOTA_REC:
|
||||
nota = nota_read_record(&n, nota);
|
||||
*tmp = JS_NewObject(js);
|
||||
for (int i = 0; i < n; i++) {
|
||||
nota = nota_read_text(&str, nota);
|
||||
JSValue prop_key = JS_NewString(js, str);
|
||||
nota = js_do_nota_decode(js, &ret2, nota, *tmp, prop_key, reviver);
|
||||
JS_SetPropertyStr(js, *tmp, str, ret2);
|
||||
JS_FreeValue(js, prop_key);
|
||||
free(str);
|
||||
}
|
||||
break;
|
||||
case NOTA_INT:
|
||||
nota = nota_read_int(&n, nota);
|
||||
*tmp = JS_NewInt64(js, n);
|
||||
break;
|
||||
case NOTA_SYM:
|
||||
nota = nota_read_sym(&b, nota);
|
||||
if (b == NOTA_PRIVATE) {
|
||||
JSValue inner;
|
||||
nota = js_do_nota_decode(js, &inner, nota, holder, JS_NULL, reviver);
|
||||
JSValue obj = JS_NewObject(js);
|
||||
cell_rt *crt = JS_GetContextOpaque(js);
|
||||
JS_SetProperty(js, obj, crt->actor_sym, inner);
|
||||
*tmp = obj;
|
||||
} else {
|
||||
switch(b) {
|
||||
case NOTA_NULL: *tmp = JS_NULL; break;
|
||||
case NOTA_FALSE: *tmp = JS_NewBool(js, 0); break;
|
||||
case NOTA_TRUE: *tmp = JS_NewBool(js, 1); break;
|
||||
default: *tmp = JS_NULL; break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
case NOTA_FLOAT:
|
||||
nota = nota_read_float(&d, nota);
|
||||
*tmp = JS_NewFloat64(js, d);
|
||||
break;
|
||||
}
|
||||
|
||||
if (!JS_IsNull(reviver)) {
|
||||
JSValue args[2] = { JS_DupValue(js, key), JS_DupValue(js, *tmp) };
|
||||
JSValue revived = JS_Call(js, reviver, holder, 2, args);
|
||||
JS_FreeValue(js, args[0]);
|
||||
JS_FreeValue(js, args[1]);
|
||||
if (!JS_IsException(revived)) {
|
||||
JS_FreeValue(js, *tmp);
|
||||
*tmp = revived;
|
||||
} else {
|
||||
JS_FreeValue(js, revived);
|
||||
}
|
||||
}
|
||||
|
||||
return nota;
|
||||
}
|
||||
|
||||
static void nota_encode_value(NotaEncodeContext *enc, JSValueConst val, JSValueConst holder, JSValueConst key) {
|
||||
JSContext *ctx = enc->ctx;
|
||||
JSValue replaced = apply_replacer(enc, holder, key, val);
|
||||
int tag = JS_VALUE_GET_TAG(replaced);
|
||||
|
||||
switch (tag) {
|
||||
case JS_TAG_INT:
|
||||
case JS_TAG_FLOAT64: {
|
||||
double d;
|
||||
JS_ToFloat64(ctx, &d, replaced);
|
||||
nota_write_number(&enc->nb, d);
|
||||
break;
|
||||
}
|
||||
case JS_TAG_STRING: {
|
||||
const char *str = JS_ToCString(ctx, replaced);
|
||||
nota_write_text(&enc->nb, str);
|
||||
JS_FreeCString(ctx, str);
|
||||
break;
|
||||
}
|
||||
case JS_TAG_BOOL:
|
||||
if (JS_VALUE_GET_BOOL(replaced)) nota_write_sym(&enc->nb, NOTA_TRUE);
|
||||
else nota_write_sym(&enc->nb, NOTA_FALSE);
|
||||
break;
|
||||
case JS_TAG_NULL:
|
||||
nota_write_sym(&enc->nb, NOTA_NULL);
|
||||
break;
|
||||
case JS_TAG_OBJECT: {
|
||||
if (js_is_blob(ctx, replaced)) {
|
||||
size_t buf_len;
|
||||
void *buf_data = js_get_blob_data(ctx, &buf_len, replaced);
|
||||
if (buf_data == -1) {
|
||||
JS_FreeValue(ctx, replaced);
|
||||
return; // JS_EXCEPTION will be handled by caller
|
||||
}
|
||||
nota_write_blob(&enc->nb, (unsigned long long)buf_len * 8, (const char*)buf_data);
|
||||
break;
|
||||
}
|
||||
|
||||
if (JS_IsArray(ctx, replaced)) {
|
||||
if (nota_stack_has(enc, replaced)) {
|
||||
enc->cycle = 1;
|
||||
break;
|
||||
}
|
||||
nota_stack_push(enc, replaced);
|
||||
int arr_len = JS_ArrayLength(ctx, replaced);
|
||||
nota_write_array(&enc->nb, arr_len);
|
||||
for (int i = 0; i < arr_len; i++) {
|
||||
JSValue elem_val = JS_GetPropertyUint32(ctx, replaced, i);
|
||||
JSValue elem_key = JS_NewInt32(ctx, i);
|
||||
nota_encode_value(enc, elem_val, replaced, elem_key);
|
||||
JS_FreeValue(ctx, elem_val);
|
||||
JS_FreeValue(ctx, elem_key);
|
||||
}
|
||||
nota_stack_pop(enc);
|
||||
break;
|
||||
}
|
||||
|
||||
cell_rt *crt = JS_GetContextOpaque(ctx);
|
||||
JSValue adata = JS_GetProperty(ctx, replaced, crt->actor_sym);
|
||||
if (!JS_IsNull(adata)) {
|
||||
nota_write_sym(&enc->nb, NOTA_PRIVATE);
|
||||
nota_encode_value(enc, adata, replaced, JS_NULL);
|
||||
JS_FreeValue(ctx, adata);
|
||||
break;
|
||||
}
|
||||
JS_FreeValue(ctx, adata);
|
||||
if (nota_stack_has(enc, replaced)) {
|
||||
enc->cycle = 1;
|
||||
break;
|
||||
}
|
||||
nota_stack_push(enc, replaced);
|
||||
|
||||
JSValue to_json = JS_GetPropertyStr(ctx, replaced, "toJSON");
|
||||
if (JS_IsFunction(ctx, to_json)) {
|
||||
JSValue result = JS_Call(ctx, to_json, replaced, 0, NULL);
|
||||
JS_FreeValue(ctx, to_json);
|
||||
if (!JS_IsException(result)) {
|
||||
nota_encode_value(enc, result, holder, key);
|
||||
JS_FreeValue(ctx, result);
|
||||
} else {
|
||||
nota_write_sym(&enc->nb, NOTA_NULL);
|
||||
}
|
||||
nota_stack_pop(enc);
|
||||
break;
|
||||
}
|
||||
JS_FreeValue(ctx, to_json);
|
||||
|
||||
JSPropertyEnum *ptab;
|
||||
uint32_t plen;
|
||||
if (JS_GetOwnPropertyNames(ctx, &ptab, &plen, replaced, JS_GPN_ENUM_ONLY | JS_GPN_STRING_MASK) < 0) {
|
||||
nota_write_sym(&enc->nb, NOTA_NULL);
|
||||
nota_stack_pop(enc);
|
||||
break;
|
||||
}
|
||||
|
||||
uint32_t non_function_count = 0;
|
||||
for (uint32_t i = 0; i < plen; i++) {
|
||||
JSValue prop_val = JS_GetProperty(ctx, replaced, ptab[i].atom);
|
||||
if (!JS_IsFunction(ctx, prop_val)) non_function_count++;
|
||||
JS_FreeValue(ctx, prop_val);
|
||||
}
|
||||
|
||||
nota_write_record(&enc->nb, non_function_count);
|
||||
for (uint32_t i = 0; i < plen; i++) {
|
||||
JSValue prop_val = JS_GetProperty(ctx, replaced, ptab[i].atom);
|
||||
if (!JS_IsFunction(ctx, prop_val)) {
|
||||
const char *prop_name = JS_AtomToCString(ctx, ptab[i].atom);
|
||||
JSValue prop_key = JS_AtomToValue(ctx, ptab[i].atom);
|
||||
nota_write_text(&enc->nb, prop_name);
|
||||
nota_encode_value(enc, prop_val, replaced, prop_key);
|
||||
JS_FreeCString(ctx, prop_name);
|
||||
JS_FreeValue(ctx, prop_key);
|
||||
}
|
||||
JS_FreeValue(ctx, prop_val);
|
||||
JS_FreeAtom(ctx, ptab[i].atom);
|
||||
}
|
||||
js_free(ctx, ptab);
|
||||
nota_stack_pop(enc);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
nota_write_sym(&enc->nb, NOTA_NULL);
|
||||
break;
|
||||
}
|
||||
JS_FreeValue(ctx, replaced);
|
||||
}
|
||||
|
||||
void *value2nota(JSContext *ctx, JSValue v) {
|
||||
NotaEncodeContext enc_s, *enc = &enc_s;
|
||||
enc->ctx = ctx;
|
||||
enc->visitedStack = JS_NewArray(ctx);
|
||||
enc->cycle = 0;
|
||||
enc->replacer = JS_NULL;
|
||||
|
||||
nota_buffer_init(&enc->nb, 128);
|
||||
nota_encode_value(enc, v, JS_NULL, JS_NewString(ctx, ""));
|
||||
|
||||
if (enc->cycle) {
|
||||
JS_FreeValue(ctx, enc->visitedStack);
|
||||
nota_buffer_free(&enc->nb);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
JS_FreeValue(ctx, enc->visitedStack);
|
||||
void *data_ptr = enc->nb.data;
|
||||
enc->nb.data = NULL;
|
||||
nota_buffer_free(&enc->nb);
|
||||
return data_ptr;
|
||||
}
|
||||
|
||||
JSValue nota2value(JSContext *js, void *nota) {
|
||||
if (!nota) return JS_NULL;
|
||||
JSValue ret;
|
||||
JSValue holder = JS_NewObject(js);
|
||||
js_do_nota_decode(js, &ret, nota, holder, JS_NewString(js, ""), JS_NULL);
|
||||
JS_FreeValue(js, holder);
|
||||
return ret;
|
||||
}
|
||||
|
||||
static JSValue js_nota_tostring(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) {
|
||||
size_t len;
|
||||
void *nota = js_get_blob_data(ctx, &len, this_val);
|
||||
if (nota == (void*)-1) return JS_EXCEPTION;
|
||||
if (!nota) return JS_NULL;
|
||||
|
||||
JSValue decoded;
|
||||
JSValue holder = JS_NewObject(ctx);
|
||||
js_do_nota_decode(ctx, &decoded, (char*)nota, holder, JS_NewString(ctx, ""), JS_NULL);
|
||||
JS_FreeValue(ctx, holder);
|
||||
|
||||
JSValue global = JS_GetGlobalObject(ctx);
|
||||
JSValue json = JS_GetPropertyStr(ctx, global, "JSON");
|
||||
JSValue stringify = JS_GetPropertyStr(ctx, json, "stringify");
|
||||
|
||||
JSValue args[3];
|
||||
args[0] = decoded;
|
||||
args[1] = JS_NULL;
|
||||
args[2] = JS_NewInt32(ctx, 1);
|
||||
|
||||
JSValue result = JS_Call(ctx, stringify, json, 3, args);
|
||||
|
||||
JS_FreeValue(ctx, stringify);
|
||||
JS_FreeValue(ctx, json);
|
||||
JS_FreeValue(ctx, global);
|
||||
JS_FreeValue(ctx, decoded);
|
||||
JS_FreeValue(ctx, args[2]);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
static JSValue js_nota_encode(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) {
|
||||
if (argc < 1) return JS_ThrowTypeError(ctx, "nota.encode requires at least 1 argument");
|
||||
|
||||
NotaEncodeContext enc_s, *enc = &enc_s;
|
||||
enc->ctx = ctx;
|
||||
enc->visitedStack = JS_NewArray(ctx);
|
||||
enc->cycle = 0;
|
||||
enc->replacer = (argc > 1 && JS_IsFunction(ctx, argv[1])) ? argv[1] : JS_NULL;
|
||||
|
||||
nota_buffer_init(&enc->nb, 128);
|
||||
nota_encode_value(enc, argv[0], JS_NULL, JS_NewString(ctx, ""));
|
||||
|
||||
if (enc->cycle) {
|
||||
JS_FreeValue(ctx, enc->visitedStack);
|
||||
nota_buffer_free(&enc->nb);
|
||||
return JS_ThrowReferenceError(ctx, "Tried to encode something to nota with a cycle.");
|
||||
}
|
||||
|
||||
JS_FreeValue(ctx, enc->visitedStack);
|
||||
size_t total_len = enc->nb.size;
|
||||
void *data_ptr = enc->nb.data;
|
||||
JSValue ret = js_new_blob_stoned_copy(ctx, (uint8_t*)data_ptr, total_len);
|
||||
|
||||
nota_buffer_free(&enc->nb);
|
||||
return ret;
|
||||
}
|
||||
|
||||
static JSValue js_nota_decode(JSContext *js, JSValueConst self, int argc, JSValueConst *argv) {
|
||||
if (argc < 1) return JS_NULL;
|
||||
|
||||
size_t len;
|
||||
unsigned char *nota = js_get_blob_data(js, &len, argv[0]);
|
||||
if (nota == -1) return JS_EXCEPTION;
|
||||
if (!nota) return JS_NULL;
|
||||
|
||||
JSValue reviver = (argc > 1 && JS_IsFunction(js, argv[1])) ? argv[1] : JS_NULL;
|
||||
JSValue ret;
|
||||
JSValue holder = JS_NewObject(js);
|
||||
js_do_nota_decode(js, &ret, (char*)nota, holder, JS_NewString(js, ""), reviver);
|
||||
JS_FreeValue(js, holder);
|
||||
return ret;
|
||||
}
|
||||
|
||||
static const JSCFunctionListEntry js_nota_funcs[] = {
|
||||
JS_CFUNC_DEF("encode", 1, js_nota_encode),
|
||||
JS_CFUNC_DEF("decode", 1, js_nota_decode),
|
||||
};
|
||||
|
||||
JSValue js_nota_use(JSContext *js) {
|
||||
JSValue export = JS_NewObject(js);
|
||||
JS_SetPropertyFunctionList(js, export, js_nota_funcs, sizeof(js_nota_funcs)/sizeof(JSCFunctionListEntry));
|
||||
return export;
|
||||
}
|
||||
@@ -577,8 +577,8 @@ static const JSCFunctionListEntry js_os_funcs[] = {
|
||||
|
||||
JSValue js_os_use(JSContext *js) {
|
||||
JS_NewClassID(&js_dylib_class_id);
|
||||
JS_NewClass(JS_GetRuntime(js), js_dylib_class_id, &js_dylib_class);
|
||||
|
||||
JS_NewClass(js, js_dylib_class_id, &js_dylib_class);
|
||||
|
||||
JSValue mod = JS_NewObject(js);
|
||||
JS_SetPropertyFunctionList(js,mod,js_os_funcs,countof(js_os_funcs));
|
||||
return mod;
|
||||
|
||||
499
internal/shop.cm
499
internal/shop.cm
@@ -1,4 +1,5 @@
|
||||
var toml = use('toml')
|
||||
|
||||
var json = use('json')
|
||||
var fd = use('fd')
|
||||
var http = use('http')
|
||||
@@ -7,6 +8,7 @@ var time = use('time')
|
||||
var js = use('js')
|
||||
var crypto = use('crypto')
|
||||
var blob = use('blob')
|
||||
|
||||
var pkg_tools = use('package')
|
||||
var os = use('os')
|
||||
var link = use('link')
|
||||
@@ -28,9 +30,9 @@ function put_into_cache(content, obj)
|
||||
|
||||
function ensure_dir(path) {
|
||||
if (fd.stat(path).isDirectory) return
|
||||
var parts = path.split('/')
|
||||
var current = path.startsWith('/') ? '/' : ''
|
||||
for (var i = 0; i < parts.length; i++) {
|
||||
var parts = array(path, '/')
|
||||
var current = starts_with(path, '/') ? '/' : ''
|
||||
for (var i = 0; i < length(parts); i++) {
|
||||
if (parts[i] == '') continue
|
||||
current += parts[i] + '/'
|
||||
if (!fd.stat(current).isDirectory) {
|
||||
@@ -91,8 +93,8 @@ Shop.get_reports_dir = function() {
|
||||
}
|
||||
|
||||
function get_import_package(name) {
|
||||
var parts = name.split('/')
|
||||
if (parts.length > 1)
|
||||
var parts = array(name, '/')
|
||||
if (length(parts) > 1)
|
||||
return parts[0]
|
||||
|
||||
return null
|
||||
@@ -100,24 +102,24 @@ function get_import_package(name) {
|
||||
|
||||
function is_internal_path(path)
|
||||
{
|
||||
return path && path.startsWith('internal/')
|
||||
return path && starts_with(path, 'internal/')
|
||||
}
|
||||
|
||||
function split_explicit_package_import(path)
|
||||
{
|
||||
if (!path) return null
|
||||
var parts = path.split('/')
|
||||
var parts = array(path, '/')
|
||||
|
||||
if (parts.length < 2) return null
|
||||
if (length(parts) < 2) return null
|
||||
|
||||
var looks_explicit = path.startsWith('/') || (parts[0] && parts[0].includes('.'))
|
||||
var looks_explicit = starts_with(path, '/') || (parts[0] && search(parts[0], '.') != null)
|
||||
if (!looks_explicit) return null
|
||||
|
||||
// Find the longest prefix that is an installed package
|
||||
for (var i = parts.length - 1; i >= 1; i--) {
|
||||
var pkg_candidate = parts.slice(0, i).join('/')
|
||||
var mod_path = parts.slice(i).join('/')
|
||||
if (!mod_path || mod_path.length == 0) continue
|
||||
for (var i = length(parts) - 1; i >= 1; i--) {
|
||||
var pkg_candidate = text(array(parts, 0, i), '/')
|
||||
var mod_path = text(array(parts, i), '/')
|
||||
if (!mod_path || length(mod_path) == 0) continue
|
||||
|
||||
var candidate_dir = get_packages_dir() + '/' + safe_package_path(pkg_candidate)
|
||||
if (fd.is_file(candidate_dir + '/cell.toml'))
|
||||
@@ -141,7 +143,7 @@ function package_in_shop(package) {
|
||||
function abs_path_to_package(package_dir)
|
||||
{
|
||||
if (!fd.is_file(package_dir + '/cell.toml'))
|
||||
throw new Error('Not a valid package directory (no cell.toml): ' + package_dir)
|
||||
throw Error('Not a valid package directory (no cell.toml): ' + package_dir)
|
||||
|
||||
var packages_prefix = get_packages_dir() + '/'
|
||||
var core_dir = packages_prefix + core_package
|
||||
@@ -158,8 +160,15 @@ function abs_path_to_package(package_dir)
|
||||
}
|
||||
}
|
||||
|
||||
if (package_dir.startsWith(packages_prefix))
|
||||
return package_dir.substring(packages_prefix.length)
|
||||
if (starts_with(package_dir, packages_prefix))
|
||||
return text(package_dir, length(packages_prefix))
|
||||
|
||||
// Check if this local path is the target of a link
|
||||
// If so, return the canonical package name (link origin) instead
|
||||
var link_origin = link.get_origin(package_dir)
|
||||
if (link_origin) {
|
||||
return link_origin
|
||||
}
|
||||
|
||||
// in this case, the dir is the package
|
||||
if (package_in_shop(package_dir))
|
||||
@@ -188,9 +197,9 @@ Shop.file_info = function(file) {
|
||||
name: null
|
||||
}
|
||||
|
||||
if (file.endsWith(MOD_EXT))
|
||||
if (ends_with(file, MOD_EXT))
|
||||
info.is_module = true
|
||||
else if (file.endsWith(ACTOR_EXT))
|
||||
else if (ends_with(file, ACTOR_EXT))
|
||||
info.is_actor = true
|
||||
|
||||
// Find package directory and determine package name
|
||||
@@ -199,11 +208,11 @@ Shop.file_info = function(file) {
|
||||
info.package = abs_path_to_package(pkg_dir)
|
||||
|
||||
if (info.is_actor)
|
||||
info.name = file.substring(pkg_dir.length + 1, file.length - ACTOR_EXT.length)
|
||||
info.name = text(file, length(pkg_dir) + 1, length(file) - length(ACTOR_EXT))
|
||||
else if (info.is_module)
|
||||
info.name = file.substring(pkg_dir.length + 1, file.length - MOD_EXT.length)
|
||||
info.name = text(file, length(pkg_dir) + 1, length(file) - length(MOD_EXT))
|
||||
else
|
||||
info.name = file.substring(pkg_dir.length + 1)
|
||||
info.name = text(file, length(pkg_dir) + 1)
|
||||
}
|
||||
|
||||
return info
|
||||
@@ -211,9 +220,9 @@ Shop.file_info = function(file) {
|
||||
|
||||
function get_import_name(path)
|
||||
{
|
||||
var parts = path.split('/')
|
||||
if (parts.length < 2) return null
|
||||
return parts.slice(1).join('/')
|
||||
var parts = array(path, '/')
|
||||
if (length(parts) < 2) return null
|
||||
return text(array(parts, 1), '/')
|
||||
}
|
||||
|
||||
// Given a path like 'prosperon/sprite' and a package context,
|
||||
@@ -239,14 +248,14 @@ function safe_package_path(pkg)
|
||||
{
|
||||
// For absolute paths, replace / with _ to create a valid directory name
|
||||
// Also replace @ with _
|
||||
if (pkg && pkg.startsWith('/'))
|
||||
return pkg.replaceAll('/', '_').replaceAll('@', '_')
|
||||
return pkg.replaceAll('@', '_')
|
||||
if (pkg && starts_with(pkg, '/'))
|
||||
return replace(replace(pkg, '/', '_'), '@', '_')
|
||||
return replace(pkg, '@', '_')
|
||||
}
|
||||
|
||||
function package_cache_path(pkg)
|
||||
{
|
||||
return global_shop_path + '/cache/' + pkg.replaceAll('/', '_').replaceAll('@', '_')
|
||||
return global_shop_path + '/cache/' + replace(replace(pkg, '/', '_'), '@', '_')
|
||||
}
|
||||
|
||||
function get_shared_lib_path()
|
||||
@@ -266,7 +275,7 @@ Shop.load_lock = function() {
|
||||
return {}
|
||||
|
||||
var content = text(fd.slurp(path))
|
||||
if (!content.length) return {}
|
||||
if (!length(content)) return {}
|
||||
|
||||
_lock = toml.decode(content)
|
||||
|
||||
@@ -276,26 +285,26 @@ Shop.load_lock = function() {
|
||||
// Save lock.toml configuration (to global shop)
|
||||
Shop.save_lock = function(lock) {
|
||||
var path = global_shop_path + '/lock.toml'
|
||||
fd.slurpwrite(path, stone(new blob(toml.encode(lock))));
|
||||
fd.slurpwrite(path, stone(blob(toml.encode(lock))));
|
||||
}
|
||||
|
||||
|
||||
// Get information about how to resolve a package
|
||||
// Local packages always start with /
|
||||
Shop.resolve_package_info = function(pkg) {
|
||||
if (pkg.startsWith('/')) return 'local'
|
||||
if (pkg.includes('gitea')) return 'gitea'
|
||||
if (starts_with(pkg, '/')) return 'local'
|
||||
if (search(pkg, 'gitea') != null) return 'gitea'
|
||||
return null
|
||||
}
|
||||
|
||||
// Verify if a package name is valid and return status
|
||||
Shop.verify_package_name = function(pkg) {
|
||||
if (!pkg) throw new Error("Empty package name")
|
||||
if (pkg == 'local') throw new Error("local is not a valid package name")
|
||||
if (pkg == 'core') throw new Error("core is not a valid package name")
|
||||
if (!pkg) throw Error("Empty package name")
|
||||
if (pkg == 'local') throw Error("local is not a valid package name")
|
||||
if (pkg == 'core') throw Error("core is not a valid package name")
|
||||
|
||||
if (pkg.includes('://'))
|
||||
throw new Error(`Invalid package name: ${pkg}; did you mean ${pkg.split('://')[1]}?`)
|
||||
if (search(pkg, '://') != null)
|
||||
throw Error(`Invalid package name: ${pkg}; did you mean ${array(pkg, '://')[1]}?`)
|
||||
}
|
||||
|
||||
// Convert module package to download URL
|
||||
@@ -303,7 +312,7 @@ Shop.get_download_url = function(pkg, commit_hash) {
|
||||
var info = Shop.resolve_package_info(pkg)
|
||||
|
||||
if (info == 'gitea') {
|
||||
var parts = pkg.split('/')
|
||||
var parts = array(pkg, '/')
|
||||
var host = parts[0]
|
||||
var user = parts[1]
|
||||
var repo = parts[2]
|
||||
@@ -319,7 +328,7 @@ Shop.get_api_url = function(pkg) {
|
||||
var info = Shop.resolve_package_info(pkg)
|
||||
|
||||
if (info == 'gitea') {
|
||||
var parts = pkg.split('/')
|
||||
var parts = array(pkg, '/')
|
||||
var host = parts[0]
|
||||
var user = parts[1]
|
||||
var repo = parts[2]
|
||||
@@ -338,7 +347,7 @@ Shop.extract_commit_hash = function(pkg, response) {
|
||||
var data = json.decode(response)
|
||||
|
||||
if (info == 'gitea') {
|
||||
if (isa(data, array))
|
||||
if (is_array(data))
|
||||
data = data[0]
|
||||
return data.commit && data.commit.id
|
||||
}
|
||||
@@ -353,11 +362,6 @@ var open_dls = {}
|
||||
// These map to $_ properties in engine.cm
|
||||
var SHOP_DEFAULT_INJECT = ['$self', '$overling', '$clock', '$delay', '$start', '$receiver', '$contact', '$portal', '$time_limit', '$couple', '$stop', '$unneeded', '$connection', '$fd']
|
||||
|
||||
function strip_dollar(name) {
|
||||
if (name && name[0] == '$') return name.substring(1)
|
||||
return name
|
||||
}
|
||||
|
||||
// Decide what a given module is allowed to see.
|
||||
// This is the capability gate - tweak as needed.
|
||||
Shop.script_inject_for = function(file_info) {
|
||||
@@ -374,19 +378,45 @@ Shop.get_script_capabilities = function(path) {
|
||||
return Shop.script_inject_for(file_info)
|
||||
}
|
||||
|
||||
function inject_params(inject) {
|
||||
if (!inject || !inject.length) return ''
|
||||
return ', ' + inject.join(', ')
|
||||
function inject_env(inject) {
|
||||
// Start with runtime functions from engine
|
||||
var env = {}
|
||||
var rt = my$_.os ? my$_.os.runtime_env : null
|
||||
if (rt) {
|
||||
for (var k in rt) {
|
||||
env[k] = rt[k]
|
||||
}
|
||||
}
|
||||
|
||||
// Add capability injections
|
||||
for (var i = 0; i < length(inject); i++) {
|
||||
var inj = inject[i]
|
||||
var key = trim(inj, '$')
|
||||
if (key == 'fd') env[key] = fd
|
||||
else env[key] = my$_[key]
|
||||
}
|
||||
return env
|
||||
}
|
||||
|
||||
function inject_values(inject) {
|
||||
var vals = []
|
||||
for (var i = 0; i < inject.length; i++) {
|
||||
var key = strip_dollar(inject[i])
|
||||
if (key == 'fd') vals.push(fd)
|
||||
else vals.push(my$_[key])
|
||||
function inject_bindings_code(inject) {
|
||||
var lines = []
|
||||
|
||||
// Runtime function bindings
|
||||
var runtime_fns = ['logical', 'some', 'every', 'starts_with', 'ends_with',
|
||||
'actor', 'is_actor', 'log', 'send',
|
||||
'fallback', 'parallel', 'race', 'sequence']
|
||||
for (var i = 0; i < length(runtime_fns); i++) {
|
||||
var fn = runtime_fns[i]
|
||||
push(lines, `var ${fn} = env["${fn}"];`)
|
||||
}
|
||||
return vals
|
||||
|
||||
// Capability bindings ($delay, $start, etc.)
|
||||
for (var i = 0; i < length(inject); i++) {
|
||||
var inj = inject[i]
|
||||
var key = trim(inj, '$')
|
||||
push(lines, `var $${key} = env["${key}"];`)
|
||||
}
|
||||
return text(lines, '\n')
|
||||
}
|
||||
|
||||
// Build the use function for a specific package context
|
||||
@@ -397,38 +427,42 @@ function make_use_fn_code(pkg_arg) {
|
||||
// for script forms, path is the canonical path of the module
|
||||
var script_form = function(path, script, pkg, inject) {
|
||||
var pkg_arg = pkg ? `'${pkg}'` : 'null'
|
||||
var params = inject_params(inject)
|
||||
|
||||
var fn = `(function setup_module(args, use${params}){ def arg = args; def PACKAGE = ${pkg_arg}; ${script}})`
|
||||
var binds = inject_bindings_code(inject)
|
||||
|
||||
var fn = `(function setup_module(args, use, env){
|
||||
def arg = args;
|
||||
def PACKAGE = ${pkg_arg};
|
||||
${binds}
|
||||
${script}
|
||||
})`
|
||||
return fn
|
||||
}
|
||||
|
||||
// Resolve module function, hashing it in the process
|
||||
// path is the exact path to the script file
|
||||
function resolve_mod_fn(path, pkg) {
|
||||
if (!fd.is_file(path)) throw new Error(`path ${path} is not a file`)
|
||||
if (!fd.is_file(path)) throw Error(`path ${path} is not a file`)
|
||||
|
||||
var file_info = Shop.file_info(path)
|
||||
var file_pkg = file_info.package
|
||||
var inject = Shop.script_inject_for(file_info)
|
||||
var content = text(fd.slurp(path))
|
||||
var script = script_form(path, content, file_pkg, inject);
|
||||
|
||||
var obj = pull_from_cache(stone(new blob(script)))
|
||||
|
||||
var obj = pull_from_cache(stone(blob(script)))
|
||||
if (obj) {
|
||||
var fn = js.compile_unblob(obj)
|
||||
return js.eval_compile(fn)
|
||||
return js.integrate(fn, null)
|
||||
}
|
||||
|
||||
|
||||
// Compile name is just for debug/stack traces
|
||||
// var compile_name = pkg ? pkg + ':' + path : 'local:' + path
|
||||
var compile_name = path
|
||||
|
||||
|
||||
var fn = js.compile(compile_name, script)
|
||||
|
||||
put_into_cache(stone(new blob(script)), js.compile_blob(fn))
|
||||
|
||||
return js.eval_compile(fn)
|
||||
|
||||
put_into_cache(stone(blob(script)), js.compile_blob(fn))
|
||||
|
||||
return js.integrate(fn, null)
|
||||
}
|
||||
|
||||
// given a path and a package context
|
||||
@@ -463,7 +497,7 @@ function resolve_locator(path, ctx)
|
||||
// If ctx is an absolute path (starts with /), use it directly
|
||||
// Otherwise, look it up in the packages directory
|
||||
var ctx_dir
|
||||
if (ctx.startsWith('/')) {
|
||||
if (starts_with(ctx, '/')) {
|
||||
ctx_dir = ctx
|
||||
} else {
|
||||
ctx_dir = get_packages_dir() + '/' + safe_package_path(ctx)
|
||||
@@ -510,25 +544,17 @@ function resolve_locator(path, ctx)
|
||||
|
||||
// Generate symbol name for a C module file
|
||||
// Uses the same format as Shop.c_symbol_for_file
|
||||
// Resolves linked packages to their actual target first
|
||||
// Symbol names are based on canonical package names, not link targets
|
||||
function make_c_symbol(pkg, file) {
|
||||
// Check if this package is linked - if so, use the link target for symbol name
|
||||
var link_target = link.get_target(pkg)
|
||||
var resolved_pkg = link_target ? link_target : pkg
|
||||
|
||||
var pkg_safe = resolved_pkg.replace(/\//g, '_').replace(/\./g, '_').replace(/-/g, '_')
|
||||
var file_safe = file.replace(/\//g, '_').replace(/\./g, '_').replace(/-/g, '_')
|
||||
var pkg_safe = replace(replace(replace(pkg, '/', '_'), '.', '_'), '-', '_')
|
||||
var file_safe = replace(replace(replace(file, '/', '_'), '.', '_'), '-', '_')
|
||||
return 'js_' + pkg_safe + '_' + file_safe + '_use'
|
||||
}
|
||||
|
||||
// Get the library path for a package in .cell/lib
|
||||
// Resolves linked packages to their actual target first
|
||||
// Library names are based on canonical package names, not link targets
|
||||
function get_lib_path(pkg) {
|
||||
// Check if this package is linked - if so, use the link target
|
||||
var link_target = link.get_target(pkg)
|
||||
var resolved_pkg = link_target ? link_target : pkg
|
||||
|
||||
var lib_name = resolved_pkg.replace(/\//g, '_').replace(/\./g, '_').replace(/-/g, '_')
|
||||
var lib_name = replace(replace(replace(pkg, '/', '_'), '.', '_'), '-', '_')
|
||||
return global_shop_path + '/lib/' + lib_name + dylib_ext
|
||||
}
|
||||
|
||||
@@ -540,34 +566,43 @@ Shop.open_package_dylib = function(pkg) {
|
||||
|
||||
var link_target = link.get_target(pkg)
|
||||
var resolved_pkg = link_target ? link_target : pkg
|
||||
|
||||
|
||||
var pkg_dir;
|
||||
if (resolved_pkg.startsWith('/')) {
|
||||
if (starts_with(resolved_pkg, '/')) {
|
||||
pkg_dir = resolved_pkg
|
||||
} else {
|
||||
pkg_dir = get_packages_dir() + '/' + safe_package_path(resolved_pkg)
|
||||
}
|
||||
|
||||
|
||||
var toml_path = pkg_dir + '/cell.toml'
|
||||
if (fd.is_file(toml_path)) {
|
||||
try {
|
||||
var content = text(fd.slurp(toml_path))
|
||||
var cfg = toml.decode(content)
|
||||
if (cfg.dependencies) {
|
||||
for (var alias in cfg.dependencies) {
|
||||
arrfor(array(cfg.dependencies), function(alias, i) {
|
||||
var dep_pkg = cfg.dependencies[alias]
|
||||
Shop.open_package_dylib(dep_pkg)
|
||||
}
|
||||
try {
|
||||
Shop.open_package_dylib(dep_pkg)
|
||||
} catch (dep_e) {
|
||||
// Dependency dylib load failed, continue with others
|
||||
}
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore errors reading cell.toml
|
||||
// Error reading toml, continue
|
||||
}
|
||||
}
|
||||
|
||||
var dl_path = get_lib_path(pkg)
|
||||
if (fd.is_file(dl_path)) {
|
||||
if (!open_dls[dl_path]) {
|
||||
open_dls[dl_path] = os.dylib_open(dl_path)
|
||||
try {
|
||||
open_dls[dl_path] = os.dylib_open(dl_path)
|
||||
} catch (e) {
|
||||
dylib_visited[pkg] = false
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -575,20 +610,19 @@ Shop.open_package_dylib = function(pkg) {
|
||||
// Resolve a C symbol by searching:
|
||||
// 1. If package_context is null, only check core internal symbols
|
||||
// 2. Otherwise: own package (internal then dylib) -> other packages (internal then dylib) -> core (internal only)
|
||||
// Core is never loaded as a dynamic library via dlopen
|
||||
function resolve_c_symbol(path, package_context)
|
||||
{
|
||||
var explicit = split_explicit_package_import(path)
|
||||
if (explicit) {
|
||||
if (is_internal_path(explicit.path) && package_context && explicit.package != package_context)
|
||||
explicit = null
|
||||
}
|
||||
if (explicit) {
|
||||
var sym = make_c_symbol(explicit.package, explicit.path)
|
||||
if (os.internal_exists(sym)) {
|
||||
return {
|
||||
symbol: function() { return os.load_internal(sym) },
|
||||
scope: SCOPE_PACKAGE,
|
||||
// Core is never loaded as a dynamic library via dlopen
|
||||
function resolve_c_symbol(path, package_context) {
|
||||
var explicit = split_explicit_package_import(path)
|
||||
if (explicit) {
|
||||
if (is_internal_path(explicit.path) && package_context && explicit.package != package_context)
|
||||
explicit = null
|
||||
}
|
||||
if (explicit) {
|
||||
var sym = make_c_symbol(explicit.package, explicit.path)
|
||||
if (os.internal_exists(sym)) {
|
||||
return {
|
||||
symbol: function() { return os.load_internal(sym) },
|
||||
scope: SCOPE_PACKAGE,
|
||||
package: explicit.package,
|
||||
path: sym
|
||||
}
|
||||
@@ -608,7 +642,7 @@ Shop.open_package_dylib = function(pkg) {
|
||||
|
||||
// If no package context, only check core internal symbols
|
||||
if (!package_context || package_context == 'core') {
|
||||
path = path.replace('/', '_')
|
||||
path = replace(path, '/', '_')
|
||||
var core_sym = `js_${path}_use`
|
||||
if (os.internal_exists(core_sym)) {
|
||||
return {
|
||||
@@ -619,7 +653,7 @@ Shop.open_package_dylib = function(pkg) {
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
// 1. Check own package first (internal, then dylib)
|
||||
var sym = make_c_symbol(package_context, path)
|
||||
if (os.internal_exists(sym)) {
|
||||
@@ -640,10 +674,10 @@ Shop.open_package_dylib = function(pkg) {
|
||||
path: sym
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (is_internal_path(path))
|
||||
return null
|
||||
|
||||
|
||||
// 2. Check aliased package imports (e.g. 'prosperon/sprite')
|
||||
var pkg_alias = get_import_package(path)
|
||||
if (pkg_alias) {
|
||||
@@ -651,7 +685,7 @@ Shop.open_package_dylib = function(pkg) {
|
||||
if (canon_pkg) {
|
||||
var mod_name = get_import_name(path)
|
||||
var sym = make_c_symbol(canon_pkg, mod_name)
|
||||
|
||||
|
||||
// Check internal first
|
||||
if (os.internal_exists(sym)) {
|
||||
return {
|
||||
@@ -661,7 +695,7 @@ Shop.open_package_dylib = function(pkg) {
|
||||
path: sym
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Then check dylib
|
||||
Shop.open_package_dylib(canon_pkg)
|
||||
var dl_path = get_lib_path(canon_pkg)
|
||||
@@ -675,9 +709,9 @@ Shop.open_package_dylib = function(pkg) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 3. Check core internal symbols (core is never a dynamic library)
|
||||
var core_sym = `js_${path}_use`
|
||||
var core_sym = `js_${replace(path, '/', '_')}_use`
|
||||
if (os.internal_exists(core_sym)) {
|
||||
return {
|
||||
symbol: function() { return os.load_internal(core_sym) },
|
||||
@@ -685,7 +719,7 @@ Shop.open_package_dylib = function(pkg) {
|
||||
path: core_sym
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -694,13 +728,13 @@ var module_info_cache = {}
|
||||
|
||||
function resolve_module_info(path, package_context) {
|
||||
var lookup_key = package_context ? package_context + ':' + path : ':' + path
|
||||
|
||||
|
||||
if (module_info_cache[lookup_key])
|
||||
return module_info_cache[lookup_key]
|
||||
|
||||
|
||||
var c_resolve = resolve_c_symbol(path, package_context) || {scope:999}
|
||||
var mod_resolve = resolve_locator(path + '.cm', package_context) || {scope:999}
|
||||
var min_scope = number.min(c_resolve.scope, mod_resolve.scope)
|
||||
var min_scope = min(c_resolve.scope, mod_resolve.scope)
|
||||
|
||||
if (min_scope == 999)
|
||||
return null
|
||||
@@ -767,6 +801,14 @@ function make_use_fn(pkg) {
|
||||
}
|
||||
}
|
||||
|
||||
// Call a C module loader and execute the entrypoint
|
||||
function call_c_module(c_resolve) {
|
||||
var mod = c_resolve.symbol()
|
||||
// if (is_function(mod))
|
||||
// return mod()
|
||||
return mod
|
||||
}
|
||||
|
||||
function execute_module(info)
|
||||
{
|
||||
var c_resolve = info.c_resolve
|
||||
@@ -777,26 +819,31 @@ function execute_module(info)
|
||||
if (mod_resolve.scope < 900) {
|
||||
var context = null
|
||||
if (c_resolve.scope < 900) {
|
||||
context = c_resolve.symbol(null, $_)
|
||||
context = call_c_module(c_resolve)
|
||||
}
|
||||
|
||||
|
||||
// Get file info to determine inject list
|
||||
var file_info = Shop.file_info(mod_resolve.path)
|
||||
var inject = Shop.script_inject_for(file_info)
|
||||
var vals = inject_values(inject)
|
||||
var env = inject_env(inject)
|
||||
var pkg = file_info.package
|
||||
var use_fn = make_use_fn(pkg)
|
||||
|
||||
// Call with signature: setup_module(args, use, ...capabilities)
|
||||
|
||||
// Call with signature: setup_module(args, use, env)
|
||||
// args is null for module loading
|
||||
used = mod_resolve.symbol.call(context, null, use_fn, ...vals)
|
||||
used = call(mod_resolve.symbol, context, [null, use_fn, env])
|
||||
} else if (c_resolve.scope < 900) {
|
||||
// C only
|
||||
used = c_resolve.symbol(null, my$_)
|
||||
used = call_c_module(c_resolve)
|
||||
} else {
|
||||
throw new Error(`Module ${info.path} could not be found`)
|
||||
} if (!used)
|
||||
throw new Error(`Module ${info} returned null`)
|
||||
throw Error(`Module ${info.path} could not be found`)
|
||||
}
|
||||
|
||||
// if (is_function(used))
|
||||
// throw Error('C module loader returned a function; did you forget to call it?')
|
||||
|
||||
if (!used)
|
||||
throw Error(`Module ${info} returned null`)
|
||||
|
||||
// stone(used)
|
||||
return used
|
||||
@@ -806,7 +853,7 @@ function get_module(path, package_context) {
|
||||
var info = resolve_module_info(path, package_context)
|
||||
|
||||
if (!info)
|
||||
throw new Error(`Module ${path} could not be found in ${package_context}`)
|
||||
throw Error(`Module ${path} could not be found in ${package_context}`)
|
||||
|
||||
return execute_module(info)
|
||||
}
|
||||
@@ -814,7 +861,7 @@ function get_module(path, package_context) {
|
||||
Shop.use = function use(path, package_context) {
|
||||
var info = resolve_module_info(path, package_context)
|
||||
if (!info)
|
||||
throw new Error(`Module ${path} could not be found in ${package_context}`)
|
||||
throw Error(`Module ${path} could not be found in ${package_context}`)
|
||||
|
||||
if (use_cache[info.cache_key])
|
||||
return use_cache[info.cache_key]
|
||||
@@ -826,7 +873,7 @@ Shop.resolve_locator = resolve_locator
|
||||
|
||||
// Get cache path for a package and commit
|
||||
function get_cache_path(pkg, commit) {
|
||||
return global_shop_path + '/cache/' + pkg.replaceAll('@','_').replaceAll('/','_') + '_' + commit + '.zip'
|
||||
return global_shop_path + '/cache/' + replace(replace(pkg, '@','_'), '/','_') + '_' + commit + '.zip'
|
||||
}
|
||||
|
||||
function get_package_abs_dir(package)
|
||||
@@ -855,22 +902,19 @@ function fetch_remote_hash(pkg) {
|
||||
// Returns the zip blob or null on failure
|
||||
function download_zip(pkg, commit_hash) {
|
||||
var cache_path = get_cache_path(pkg, commit_hash)
|
||||
|
||||
|
||||
var download_url = Shop.get_download_url(pkg, commit_hash)
|
||||
if (!download_url) {
|
||||
log.error("Could not determine download URL for " + pkg)
|
||||
return null
|
||||
}
|
||||
|
||||
log.console("Downloading from " + download_url)
|
||||
try {
|
||||
var zip_blob = http.fetch(download_url)
|
||||
log.console(`putting to ${cache_path}`)
|
||||
fd.slurpwrite(cache_path, zip_blob)
|
||||
log.console("Cached to " + cache_path)
|
||||
return zip_blob
|
||||
} catch (e) {
|
||||
log.error(e)
|
||||
log.error("Download failed for " + pkg + ": " + e)
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -885,39 +929,58 @@ function get_cached_zip(pkg, commit_hash) {
|
||||
}
|
||||
|
||||
// Fetch: Ensure the zip on disk matches what's in the lock file
|
||||
// For local packages, this is a no-op (returns true)
|
||||
// For local packages, this is a no-op
|
||||
// For remote packages, downloads the zip if not present or hash mismatch
|
||||
// Returns true on success
|
||||
// Returns: { status: 'local'|'cached'|'downloaded'|'error', message: string }
|
||||
Shop.fetch = function(pkg) {
|
||||
var lock = Shop.load_lock()
|
||||
var lock_entry = lock[pkg]
|
||||
var info = Shop.resolve_package_info(pkg)
|
||||
|
||||
if (info == 'local') return null
|
||||
if (info == 'local') {
|
||||
return { status: 'local' }
|
||||
}
|
||||
|
||||
// No lock entry - can't fetch without knowing what commit
|
||||
if (!lock_entry || !lock_entry.commit)
|
||||
throw new Error("No lock entry for " + pkg + " - run update first")
|
||||
|
||||
if (!lock_entry || !lock_entry.commit) {
|
||||
return { status: 'error', message: "No lock entry for " + pkg + " - run update first" }
|
||||
}
|
||||
|
||||
var commit = lock_entry.commit
|
||||
var expected_hash = lock_entry.zip_hash
|
||||
|
||||
|
||||
// Check if we have the zip cached
|
||||
var zip_blob = get_cached_zip(pkg, commit)
|
||||
|
||||
|
||||
if (zip_blob) {
|
||||
// Verify hash matches
|
||||
var actual_hash = text(crypto.blake2(zip_blob), 'h')
|
||||
if (actual_hash == expected_hash)
|
||||
return true
|
||||
|
||||
log.console("Zip hash mismatch for " + pkg + ", re-fetching...")
|
||||
// If we have a hash on record, verify it
|
||||
if (expected_hash) {
|
||||
var actual_hash = text(crypto.blake2(zip_blob), 'h')
|
||||
if (actual_hash == expected_hash) {
|
||||
return { status: 'cached' }
|
||||
}
|
||||
log.console("Zip hash mismatch for " + pkg + ", re-fetching...")
|
||||
} else {
|
||||
// No hash stored yet - compute and store it
|
||||
var actual_hash = text(crypto.blake2(zip_blob), 'h')
|
||||
lock_entry.zip_hash = actual_hash
|
||||
Shop.save_lock(lock)
|
||||
return { status: 'cached' }
|
||||
}
|
||||
}
|
||||
|
||||
// Download the zip
|
||||
download_zip(pkg, commit)
|
||||
|
||||
return true
|
||||
// Download the zip
|
||||
var new_zip = download_zip(pkg, commit)
|
||||
if (!new_zip) {
|
||||
return { status: 'error', message: "Failed to download " + pkg }
|
||||
}
|
||||
|
||||
// Store the hash
|
||||
var new_hash = text(crypto.blake2(new_zip), 'h')
|
||||
lock_entry.zip_hash = new_hash
|
||||
Shop.save_lock(lock)
|
||||
|
||||
return { status: 'downloaded' }
|
||||
}
|
||||
|
||||
// Extract: Extract a package to its target directory
|
||||
@@ -927,7 +990,7 @@ Shop.fetch = function(pkg) {
|
||||
// Returns true on success
|
||||
Shop.extract = function(pkg) {
|
||||
var target_dir = get_package_abs_dir(pkg)
|
||||
|
||||
|
||||
// Check if this package is linked
|
||||
var link_target = link.get_target(pkg)
|
||||
if (link_target) {
|
||||
@@ -935,7 +998,7 @@ Shop.extract = function(pkg) {
|
||||
link.sync_one(pkg, link_target)
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
var info = Shop.resolve_package_info(pkg)
|
||||
|
||||
if (info == 'local') {
|
||||
@@ -948,13 +1011,33 @@ Shop.extract = function(pkg) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check if already extracted at correct commit
|
||||
var lock = Shop.load_lock()
|
||||
var lock_entry = lock[pkg]
|
||||
if (lock_entry && lock_entry.commit) {
|
||||
var extracted_commit_file = target_dir + '/.cell_commit'
|
||||
if (fd.is_file(extracted_commit_file)) {
|
||||
var extracted_commit = trim(text(fd.slurp(extracted_commit_file)))
|
||||
if (extracted_commit == lock_entry.commit) {
|
||||
// Already extracted at this commit, skip
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var zip_blob = get_package_zip(pkg)
|
||||
|
||||
if (!zip_blob)
|
||||
throw new Error("No zip blob available for " + pkg)
|
||||
throw Error("No zip blob available for " + pkg)
|
||||
|
||||
// Extract zip for remote package
|
||||
install_zip(zip_blob, target_dir)
|
||||
|
||||
// Write marker file with the extracted commit
|
||||
if (lock_entry && lock_entry.commit) {
|
||||
fd.slurpwrite(target_dir + '/.cell_commit', stone(blob(lock_entry.commit)))
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -986,8 +1069,20 @@ Shop.update = function(pkg) {
|
||||
|
||||
log.console(`checking ${pkg}`)
|
||||
|
||||
if (info == 'local') return {
|
||||
updated: time.number()
|
||||
if (info == 'local') {
|
||||
// Check if local path exists
|
||||
if (!fd.is_dir(pkg)) {
|
||||
log.console(` Local path does not exist: ${pkg}`)
|
||||
return null
|
||||
}
|
||||
// Local packages always get a lock entry
|
||||
var new_entry = {
|
||||
type: 'local',
|
||||
updated: time.number()
|
||||
}
|
||||
lock[pkg] = new_entry
|
||||
Shop.save_lock(lock)
|
||||
return new_entry
|
||||
}
|
||||
|
||||
var local_commit = lock_entry ? lock_entry.commit : null
|
||||
@@ -996,14 +1091,14 @@ Shop.update = function(pkg) {
|
||||
log.console(`local commit: ${local_commit}`)
|
||||
log.console(`remote commit: ${remote_commit}`)
|
||||
|
||||
if (local_commit == remote_commit)
|
||||
return null
|
||||
|
||||
if (!remote_commit) {
|
||||
log.error("Could not resolve commit for " + pkg)
|
||||
return null
|
||||
}
|
||||
|
||||
if (local_commit == remote_commit)
|
||||
return null
|
||||
|
||||
var new_entry = {
|
||||
type: info,
|
||||
commit: remote_commit,
|
||||
@@ -1018,29 +1113,36 @@ Shop.update = function(pkg) {
|
||||
|
||||
function install_zip(zip_blob, target_dir) {
|
||||
var zip = miniz.read(zip_blob)
|
||||
if (!zip) throw new Error("Failed to read zip archive")
|
||||
|
||||
if (!zip) throw Error("Failed to read zip archive")
|
||||
|
||||
if (fd.is_link(target_dir)) fd.unlink(target_dir)
|
||||
if (fd.is_dir(target_dir)) fd.rmdir(target_dir, 1)
|
||||
|
||||
|
||||
log.console("Extracting to " + target_dir)
|
||||
ensure_dir(target_dir)
|
||||
|
||||
|
||||
var count = zip.count()
|
||||
|
||||
var created_dirs = {}
|
||||
|
||||
for (var i = 0; i < count; i++) {
|
||||
if (zip.is_directory(i)) continue
|
||||
var filename = zip.get_filename(i)
|
||||
var parts = filename.split('/')
|
||||
if (parts.length <= 1) continue
|
||||
|
||||
parts.shift()
|
||||
var rel_path = parts.join('/')
|
||||
var slash_pos = search(filename, '/')
|
||||
if (slash_pos == null) continue
|
||||
if (slash_pos + 1 >= length(filename)) continue
|
||||
var rel_path = text(filename, slash_pos + 1)
|
||||
var full_path = target_dir + '/' + rel_path
|
||||
var dir_path = full_path.substring(0, full_path.lastIndexOf('/'))
|
||||
|
||||
ensure_dir(dir_path)
|
||||
fd.slurpwrite(full_path, zip.slurp(filename))
|
||||
var dir_path = fd.dirname(full_path)
|
||||
|
||||
if (!created_dirs[dir_path]) {
|
||||
ensure_dir(dir_path)
|
||||
created_dirs[dir_path] = true
|
||||
}
|
||||
var file_data = zip.slurp(filename)
|
||||
|
||||
stone(file_data)
|
||||
|
||||
fd.slurpwrite(full_path, file_data)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1063,14 +1165,14 @@ Shop.get = function(pkg) {
|
||||
if (!lock[pkg]) {
|
||||
var info = Shop.resolve_package_info(pkg)
|
||||
if (!info) {
|
||||
throw new Error("Invalid package: " + pkg)
|
||||
throw Error("Invalid package: " + pkg)
|
||||
}
|
||||
|
||||
var commit = null
|
||||
if (info != 'local') {
|
||||
commit = fetch_remote_hash(pkg)
|
||||
if (!commit) {
|
||||
throw new Error("Could not resolve commit for " + pkg)
|
||||
throw Error("Could not resolve commit for " + pkg)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1112,12 +1214,14 @@ Shop.module_reload = function(path, package) {
|
||||
var old = use_cache[cache_key]
|
||||
var newmod = get_module(path, package)
|
||||
|
||||
for (var i in newmod)
|
||||
arrfor(array(newmod), function(i, idx) {
|
||||
old[i] = newmod[i]
|
||||
})
|
||||
|
||||
for (var i in old)
|
||||
arrfor(array(old), function(i, idx) {
|
||||
if (!(i in newmod))
|
||||
old[i] = null
|
||||
})
|
||||
}
|
||||
|
||||
function get_package_scripts(package)
|
||||
@@ -1125,10 +1229,10 @@ function get_package_scripts(package)
|
||||
var files = pkg_tools.list_files(package)
|
||||
var scripts = []
|
||||
|
||||
for (var i = 0; i < files.length; i++) {
|
||||
for (var i = 0; i < length(files); i++) {
|
||||
var file = files[i]
|
||||
if (file.endsWith('.cm') || file.endsWith('.ce')) {
|
||||
scripts.push(file)
|
||||
if (ends_with(file, '.cm') || ends_with(file, '.ce')) {
|
||||
push(scripts, file)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1141,8 +1245,9 @@ Shop.build_package_scripts = function(package)
|
||||
var scripts = get_package_scripts(package)
|
||||
var pkg_dir = get_package_abs_dir(package)
|
||||
|
||||
for (var script of scripts)
|
||||
arrfor(scripts, function(script, i) {
|
||||
resolve_mod_fn(pkg_dir + '/' + script, package)
|
||||
})
|
||||
}
|
||||
|
||||
Shop.list_packages = function()
|
||||
@@ -1174,22 +1279,22 @@ Shop.get_package_dir = function(pkg) {
|
||||
// e.g., c_symbol_for_file('gitea.pockle.world/john/prosperon', 'sprite.c')
|
||||
// -> 'js_gitea_pockle_world_john_prosperon_sprite_use'
|
||||
Shop.c_symbol_for_file = function(pkg, file) {
|
||||
var pkg_safe = pkg.replace(/\//g, '_').replace(/\./g, '_').replace(/-/g, '_')
|
||||
var file_safe = file.substring(0, file.lastIndexOf('.')).replace(/\//g, '_').replace(/\./g, '_').replace(/-/g, '_')
|
||||
var pkg_safe = replace(replace(replace(pkg, '/', '_'), '.', '_'), '-', '_')
|
||||
var file_safe = replace(replace(fd.stem(file), '/', '_'), '.', '_')
|
||||
return 'js_' + pkg_safe + '_' + file_safe + '_use'
|
||||
}
|
||||
|
||||
// Generate C symbol prefix for a package
|
||||
// e.g., c_symbol_prefix('gitea.pockle.world/john/prosperon') -> 'js_gitea_pockle_world_john_prosperon_'
|
||||
Shop.c_symbol_prefix = function(pkg) {
|
||||
var pkg_safe = pkg.replace(/\//g, '_').replace(/\./g, '_').replace(/-/g, '_')
|
||||
var pkg_safe = replace(replace(replace(pkg, '/', '_'), '.', '_'), '-', '_')
|
||||
return 'js_' + pkg_safe + '_'
|
||||
}
|
||||
|
||||
// Get the library name for a package (without extension)
|
||||
// e.g., 'gitea.pockle.world/john/prosperon' -> 'gitea_pockle_world_john_prosperon'
|
||||
Shop.lib_name_for_package = function(pkg) {
|
||||
return pkg.replace(/\//g, '_').replace(/\./g, '_').replace(/-/g, '_')
|
||||
return replace(replace(replace(pkg, '/', '_'), '.', '_'), '-', '_')
|
||||
}
|
||||
|
||||
// Returns { ok: bool, results: [{pkg, ok, error}] }
|
||||
@@ -1198,12 +1303,12 @@ Shop.audit_packages = function() {
|
||||
|
||||
var bad = []
|
||||
|
||||
for (var package of packages) {
|
||||
if (package == 'core') continue
|
||||
if (fd.is_dir(package)) continue
|
||||
if (fetch_remote_hash(package)) continue
|
||||
bad.push(package)
|
||||
}
|
||||
arrfor(packages, function(package, i) {
|
||||
if (package == 'core') return
|
||||
if (fd.is_dir(package)) return
|
||||
if (fetch_remote_hash(package)) return
|
||||
push(bad, package)
|
||||
})
|
||||
|
||||
return bad
|
||||
}
|
||||
@@ -1215,16 +1320,16 @@ Shop.parse_package = function(locator) {
|
||||
|
||||
// Strip version suffix if present
|
||||
var clean = locator
|
||||
if (locator.includes('@')) {
|
||||
clean = locator.split('@')[0]
|
||||
if (search(locator, '@') != null) {
|
||||
clean = array(locator, '@')[0]
|
||||
}
|
||||
|
||||
var info = Shop.resolve_package_info(clean)
|
||||
if (!info) return null
|
||||
|
||||
// Extract package name (last component of path)
|
||||
var parts = clean.split('/')
|
||||
var name = parts[parts.length - 1]
|
||||
var parts = array(clean, '/')
|
||||
var name = parts[length(parts) - 1]
|
||||
|
||||
return {
|
||||
path: clean,
|
||||
|
||||
@@ -24,7 +24,7 @@ function get_pkg_dir(package_name) {
|
||||
if (!package_name) {
|
||||
return fd.realpath('.')
|
||||
}
|
||||
if (package_name.startsWith('/')) {
|
||||
if (starts_with(package_name, '/')) {
|
||||
return package_name
|
||||
}
|
||||
var shop = use('internal/shop')
|
||||
@@ -35,9 +35,9 @@ function get_pkg_dir(package_name) {
|
||||
function ensure_dir(path) {
|
||||
if (fd.is_dir(path)) return true
|
||||
|
||||
var parts = path.split('/')
|
||||
var current = path.startsWith('/') ? '/' : ''
|
||||
for (var i = 0; i < parts.length; i++) {
|
||||
var parts = array(path, '/')
|
||||
var current = starts_with(path, '/') ? '/' : ''
|
||||
for (var i = 0; i < length(parts); i++) {
|
||||
if (parts[i] == '') continue
|
||||
current += parts[i] + '/'
|
||||
if (!fd.is_dir(current)) {
|
||||
|
||||
27
link.ce
27
link.ce
@@ -17,7 +17,7 @@ var shop = use('internal/shop')
|
||||
var fd = use('fd')
|
||||
var toml = use('toml')
|
||||
|
||||
if (args.length < 1) {
|
||||
if (length(args) < 1) {
|
||||
log.console("Usage: link <command> [args] or link [package] <target>")
|
||||
log.console("Commands:")
|
||||
log.console(" list List all active links")
|
||||
@@ -35,25 +35,25 @@ var cmd = args[0]
|
||||
if (cmd == 'list') {
|
||||
var links = link.load()
|
||||
var count = 0
|
||||
for (var k in links) {
|
||||
arrfor(array(links), function(k) {
|
||||
log.console(k + " -> " + links[k])
|
||||
count++
|
||||
}
|
||||
})
|
||||
if (count == 0) log.console("No links.")
|
||||
|
||||
} else if (cmd == 'sync') {
|
||||
log.console("Syncing links...")
|
||||
var result = link.sync_all(shop)
|
||||
log.console("Synced " + result.synced + " link(s)")
|
||||
if (result.errors.length > 0) {
|
||||
if (length(result.errors) > 0) {
|
||||
log.console("Errors:")
|
||||
for (var i = 0; i < result.errors.length; i++) {
|
||||
for (var i = 0; i < length(result.errors); i++) {
|
||||
log.console(" " + result.errors[i])
|
||||
}
|
||||
}
|
||||
|
||||
} else if (cmd == 'delete' || cmd == 'rm') {
|
||||
if (args.length < 2) {
|
||||
if (length(args) < 2) {
|
||||
log.console("Usage: link delete <package>")
|
||||
$stop()
|
||||
return
|
||||
@@ -92,7 +92,7 @@ if (cmd == 'list') {
|
||||
}
|
||||
|
||||
var arg1 = args[start_idx]
|
||||
var arg2 = (args.length > start_idx + 1) ? args[start_idx + 1] : null
|
||||
var arg2 = (length(args) > start_idx + 1) ? args[start_idx + 1] : null
|
||||
|
||||
if (!arg1) {
|
||||
log.console("Error: target or package required")
|
||||
@@ -108,13 +108,13 @@ if (cmd == 'list') {
|
||||
// Resolve target if it's a local path
|
||||
if (target == '.' || fd.is_dir(target)) {
|
||||
target = fd.realpath(target)
|
||||
} else if (target.startsWith('./') || target.startsWith('../')) {
|
||||
} else if (starts_with(target, './') || starts_with(target, '../')) {
|
||||
// Relative path that doesn't exist yet - try to resolve anyway
|
||||
var cwd = fd.realpath('.')
|
||||
if (target.startsWith('./')) {
|
||||
target = cwd + target.substring(1)
|
||||
if (starts_with(target, './')) {
|
||||
target = cwd + text(target, 1)
|
||||
} else {
|
||||
// For ../ paths, let fd.realpath handle it if possible
|
||||
// For ../ paths, var fd.realpath handle it if possible
|
||||
target = fd.realpath(target) || target
|
||||
}
|
||||
}
|
||||
@@ -127,7 +127,7 @@ if (cmd == 'list') {
|
||||
// Resolve path
|
||||
if (target == '.' || fd.is_dir(target)) {
|
||||
target = fd.realpath(target)
|
||||
} else if (target.startsWith('./') || target.startsWith('../')) {
|
||||
} else if (starts_with(target, './') || starts_with(target, '../')) {
|
||||
target = fd.realpath(target) || target
|
||||
}
|
||||
|
||||
@@ -158,7 +158,7 @@ if (cmd == 'list') {
|
||||
}
|
||||
|
||||
// Validate: if target is a local path, it must have cell.toml
|
||||
if (target.startsWith('/')) {
|
||||
if (starts_with(target, '/')) {
|
||||
if (!fd.is_file(target + '/cell.toml')) {
|
||||
log.console("Error: " + target + " is not a valid package (no cell.toml)")
|
||||
$stop()
|
||||
@@ -171,6 +171,7 @@ if (cmd == 'list') {
|
||||
link.add(pkg_name, target, shop)
|
||||
} catch (e) {
|
||||
log.console("Error: " + e.message)
|
||||
log.error(e)
|
||||
$stop()
|
||||
return
|
||||
}
|
||||
|
||||
128
link.cm
128
link.cm
@@ -21,9 +21,9 @@ function get_packages_dir() {
|
||||
// return the safe path for the package
|
||||
function safe_package_path(pkg) {
|
||||
// For absolute paths, replace / with _ to create a valid directory name
|
||||
if (pkg && pkg.startsWith('/'))
|
||||
return pkg.replaceAll('/', '_').replaceAll('@', '_')
|
||||
return pkg.replaceAll('@', '_')
|
||||
if (pkg && starts_with(pkg, '/'))
|
||||
return replace(replace(pkg, '/', '_'), '@', '_')
|
||||
return replace(pkg, '@', '_')
|
||||
}
|
||||
|
||||
function get_package_abs_dir(package) {
|
||||
@@ -32,9 +32,9 @@ function get_package_abs_dir(package) {
|
||||
|
||||
function ensure_dir(path) {
|
||||
if (fd.stat(path).isDirectory) return
|
||||
var parts = path.split('/')
|
||||
var current = path.startsWith('/') ? '/' : ''
|
||||
for (var i = 0; i < parts.length; i++) {
|
||||
var parts = array(path, '/')
|
||||
var current = starts_with(path, '/') ? '/' : ''
|
||||
for (var i = 0; i < length(parts); i++) {
|
||||
if (parts[i] == '') continue
|
||||
current += parts[i] + '/'
|
||||
if (!fd.stat(current).isDirectory) {
|
||||
@@ -47,7 +47,7 @@ function ensure_dir(path) {
|
||||
// If target is a local path (starts with /), return it directly
|
||||
// If target is a package name, return the package directory
|
||||
function resolve_link_target(target) {
|
||||
if (target.startsWith('/')) {
|
||||
if (starts_with(target, '/')) {
|
||||
return target
|
||||
}
|
||||
// Target is another package - resolve to its directory
|
||||
@@ -81,34 +81,67 @@ Link.save = function(links) {
|
||||
link_cache = links
|
||||
var cfg = { links: links }
|
||||
var path = get_links_path()
|
||||
fd.slurpwrite(path, new blob(toml.encode(cfg)))
|
||||
var b = blob(toml.encode(cfg))
|
||||
stone(b)
|
||||
fd.slurpwrite(path, b)
|
||||
}
|
||||
|
||||
Link.add = function(canonical, target, shop) {
|
||||
// Validate canonical package exists in shop
|
||||
var lock = shop.load_lock()
|
||||
if (!lock[canonical]) {
|
||||
throw new Error('Package ' + canonical + ' is not installed. Install it first with: cell get ' + canonical)
|
||||
throw Error('Package ' + canonical + ' is not installed. Install it first with: cell get ' + canonical)
|
||||
}
|
||||
|
||||
|
||||
// Validate target is a valid package
|
||||
if (target.startsWith('/')) {
|
||||
if (starts_with(target, '/')) {
|
||||
// Local path - must have cell.toml
|
||||
if (!fd.is_file(target + '/cell.toml')) {
|
||||
throw new Error('Target ' + target + ' is not a valid package (no cell.toml)')
|
||||
throw Error('Target ' + target + ' is not a valid package (no cell.toml)')
|
||||
}
|
||||
} else {
|
||||
// Remote package target - ensure it's installed
|
||||
shop.get(target)
|
||||
}
|
||||
|
||||
|
||||
var links = Link.load()
|
||||
links[canonical] = target
|
||||
Link.save(links)
|
||||
|
||||
|
||||
// Create the symlink immediately
|
||||
Link.sync_one(canonical, target, shop)
|
||||
|
||||
|
||||
// Install dependencies of the linked package
|
||||
// Read the target's cell.toml to find its dependencies
|
||||
var target_path = starts_with(target, '/') ? target : get_package_abs_dir(target)
|
||||
var toml_path = target_path + '/cell.toml'
|
||||
if (fd.is_file(toml_path)) {
|
||||
try {
|
||||
var content = text(fd.slurp(toml_path))
|
||||
var cfg = toml.decode(content)
|
||||
if (cfg.dependencies) {
|
||||
arrfor(array(cfg.dependencies), function(alias) {
|
||||
var dep_locator = cfg.dependencies[alias]
|
||||
// Skip local dependencies that don't exist
|
||||
if (starts_with(dep_locator, '/') && !fd.is_dir(dep_locator)) {
|
||||
log.console(" Skipping missing local dependency: " + dep_locator)
|
||||
return
|
||||
}
|
||||
// Install the dependency if not already in shop
|
||||
try {
|
||||
shop.get(dep_locator)
|
||||
shop.extract(dep_locator)
|
||||
} catch (e) {
|
||||
log.console(` Warning: Could not install dependency ${dep_locator}: ${e.message}`)
|
||||
log.error(e)
|
||||
}
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
log.console(` Warning: Could not read dependencies from ${toml_path}`)
|
||||
}
|
||||
}
|
||||
|
||||
log.console("Linked " + canonical + " -> " + target)
|
||||
return true
|
||||
}
|
||||
@@ -133,12 +166,12 @@ Link.remove = function(canonical) {
|
||||
Link.clear = function() {
|
||||
// Remove all symlinks first
|
||||
var links = Link.load()
|
||||
for (var canonical in links) {
|
||||
arrfor(array(links), function(canonical) {
|
||||
var target_dir = get_package_abs_dir(canonical)
|
||||
if (fd.is_link(target_dir)) {
|
||||
fd.unlink(target_dir)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
Link.save({})
|
||||
log.console("Cleared all links")
|
||||
@@ -151,7 +184,7 @@ Link.sync_one = function(canonical, target, shop) {
|
||||
var link_target = resolve_link_target(target)
|
||||
|
||||
// Ensure parent directories exist
|
||||
var parent = target_dir.substring(0, target_dir.lastIndexOf('/'))
|
||||
var parent = fd.dirname(target_dir)
|
||||
ensure_dir(parent)
|
||||
|
||||
// Check current state
|
||||
@@ -177,33 +210,59 @@ Link.sync_one = function(canonical, target, shop) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Sync all links - ensure all symlinks are in place
|
||||
// Sync all links - ensure all symlinks are in place and dependencies are installed
|
||||
Link.sync_all = function(shop) {
|
||||
var links = Link.load()
|
||||
var count = 0
|
||||
var errors = []
|
||||
|
||||
for (var canonical in links) {
|
||||
|
||||
arrfor(array(links), function(canonical) {
|
||||
var target = links[canonical]
|
||||
try {
|
||||
// Validate target exists
|
||||
var link_target = resolve_link_target(target)
|
||||
if (!fd.is_dir(link_target)) {
|
||||
errors.push(canonical + ': target ' + link_target + ' does not exist')
|
||||
continue
|
||||
push(errors, canonical + ': target ' + link_target + ' does not exist')
|
||||
return
|
||||
}
|
||||
if (!fd.is_file(link_target + '/cell.toml')) {
|
||||
errors.push(canonical + ': target ' + link_target + ' is not a valid package')
|
||||
continue
|
||||
push(errors, canonical + ': target ' + link_target + ' is not a valid package')
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
Link.sync_one(canonical, target, shop)
|
||||
|
||||
// Install dependencies of the linked package
|
||||
var toml_path = link_target + '/cell.toml'
|
||||
try {
|
||||
var content = text(fd.slurp(toml_path))
|
||||
var cfg = toml.decode(content)
|
||||
if (cfg.dependencies) {
|
||||
arrfor(array(cfg.dependencies), function(alias) {
|
||||
var dep_locator = cfg.dependencies[alias]
|
||||
// Skip local dependencies that don't exist
|
||||
if (starts_with(dep_locator, '/') && !fd.is_dir(dep_locator)) {
|
||||
return
|
||||
}
|
||||
// Install the dependency if not already in shop
|
||||
try {
|
||||
shop.get(dep_locator)
|
||||
shop.extract(dep_locator)
|
||||
} catch (e) {
|
||||
// Silently continue - dependency may already be installed
|
||||
}
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
// Could not read dependencies - continue anyway
|
||||
}
|
||||
|
||||
count++
|
||||
} catch (e) {
|
||||
errors.push(canonical + ': ' + e.message)
|
||||
push(errors, canonical + ': ' + e.message)
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
return { synced: count, errors: errors }
|
||||
}
|
||||
|
||||
@@ -219,4 +278,15 @@ Link.get_target = function(canonical) {
|
||||
return links[canonical] || null
|
||||
}
|
||||
|
||||
// Get the canonical package name that links to this target (reverse lookup)
|
||||
// Returns null if no package links to this target
|
||||
Link.get_origin = function(target) {
|
||||
var links = Link.load()
|
||||
var found = null
|
||||
arrfor(array(links), function(origin) {
|
||||
if (links[origin] == target) found = origin
|
||||
})
|
||||
return found
|
||||
}
|
||||
|
||||
return Link
|
||||
|
||||
216
list.ce
216
list.ce
@@ -1,85 +1,169 @@
|
||||
// list installed packages
|
||||
// cell list -> list packages installed in this package
|
||||
// cell list all -> list all packages (including those that are there due to installed packages)
|
||||
// cell list package <name> -> list the packages for the package <name>
|
||||
// cell list [<scope>] - List packages and dependencies
|
||||
//
|
||||
// Usage:
|
||||
// cell list List dependencies of current package
|
||||
// cell list shop List all packages in shop with status
|
||||
// cell list <locator> List dependency tree for a package
|
||||
|
||||
var shop = use('internal/shop')
|
||||
var pkg = use('package')
|
||||
var link = use('link')
|
||||
var fd = use('fd')
|
||||
|
||||
var mode = 'local'
|
||||
var target_pkg = null
|
||||
|
||||
if (args && args.length > 0) {
|
||||
if (args[0] == 'all') {
|
||||
mode = 'all'
|
||||
} else if (args[0] == 'shop') {
|
||||
mode = 'shop'
|
||||
} else if (args[0] == 'package') {
|
||||
if (args.length < 2) {
|
||||
log.console("Usage: cell list package <name>")
|
||||
$stop()
|
||||
return
|
||||
}
|
||||
mode = 'package'
|
||||
target_pkg = args[1]
|
||||
} else {
|
||||
log.console("Usage:")
|
||||
log.console(" cell list : list local packages")
|
||||
log.console(" cell list all : list all recursive packages")
|
||||
log.console(" cell list package <name>: list dependencies of <name>")
|
||||
log.console(" cell list shop : list all packages in shop")
|
||||
$stop()
|
||||
return
|
||||
if (args && length(args) > 0) {
|
||||
if (args[0] == 'shop') {
|
||||
mode = 'shop'
|
||||
} else if (args[0] == '--help' || args[0] == '-h') {
|
||||
log.console("Usage: cell list [<scope>]")
|
||||
log.console("")
|
||||
log.console("List packages and dependencies.")
|
||||
log.console("")
|
||||
log.console("Scopes:")
|
||||
log.console(" (none) List dependencies of current package")
|
||||
log.console(" shop List all packages in shop with status")
|
||||
log.console(" <locator> List dependency tree for a package")
|
||||
$stop()
|
||||
} else {
|
||||
mode = 'package'
|
||||
target_pkg = args[0]
|
||||
|
||||
// Resolve local paths
|
||||
if (target_pkg == '.' || starts_with(target_pkg, './') || starts_with(target_pkg, '../') || fd.is_dir(target_pkg)) {
|
||||
var resolved = fd.realpath(target_pkg)
|
||||
if (resolved) {
|
||||
target_pkg = resolved
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var links = link.load()
|
||||
var lock = shop.load_lock()
|
||||
|
||||
function print_deps(ctx, indent) {
|
||||
indent = indent || ""
|
||||
var deps
|
||||
try {
|
||||
deps = pkg.dependencies(ctx)
|
||||
} catch (e) {
|
||||
log.console(indent + " (could not read dependencies)")
|
||||
return
|
||||
}
|
||||
|
||||
if (!deps) {
|
||||
log.console(indent + " (none)")
|
||||
return
|
||||
}
|
||||
|
||||
var aliases = array(deps)
|
||||
aliases = sort(aliases)
|
||||
|
||||
if (length(aliases) == 0) {
|
||||
log.console(indent + " (none)")
|
||||
return
|
||||
}
|
||||
|
||||
for (var i = 0; i < length(aliases); i++) {
|
||||
var alias = aliases[i]
|
||||
var locator = deps[alias]
|
||||
var link_target = links[locator]
|
||||
var lock_entry = lock[locator]
|
||||
|
||||
var line = indent + " " + alias
|
||||
if (alias != locator) {
|
||||
line += " -> " + locator
|
||||
}
|
||||
|
||||
// Add status indicators
|
||||
var status = []
|
||||
if (link_target) {
|
||||
push(status, "linked -> " + link_target)
|
||||
}
|
||||
if (lock_entry && lock_entry.commit) {
|
||||
push(status, "@" + text(lock_entry.commit, 0, 8))
|
||||
}
|
||||
if (lock_entry && lock_entry.type == 'local') {
|
||||
push(status, "local")
|
||||
}
|
||||
if (!lock_entry) {
|
||||
push(status, "not installed")
|
||||
}
|
||||
|
||||
if (length(status) > 0) {
|
||||
line += " [" + text(status, ", ") + "]"
|
||||
}
|
||||
|
||||
log.console(line)
|
||||
}
|
||||
}
|
||||
|
||||
if (mode == 'local') {
|
||||
log.console("Installed Packages (Local):")
|
||||
print_deps(null)
|
||||
log.console("Dependencies:")
|
||||
print_deps(null)
|
||||
} else if (mode == 'package') {
|
||||
// Resolve alias to canonical package path
|
||||
var canon = shop.get_canonical_package(target_pkg, null)
|
||||
if (!canon) {
|
||||
log.console("Package '" + target_pkg + "' not found in local dependencies.")
|
||||
} else {
|
||||
log.console("Dependencies for " + target_pkg + " (" + canon + "):")
|
||||
print_deps(canon)
|
||||
}
|
||||
} else if (mode == 'all') {
|
||||
log.console("All Packages:")
|
||||
var all = shop.list_packages(null)
|
||||
// list_packages returns an array of package strings (locators)
|
||||
// We want to perhaps sort them
|
||||
all.sort()
|
||||
for (var i = 0; i < all.length; i++) {
|
||||
log.console(" " + all[i])
|
||||
}
|
||||
if (all.length == 0) log.console(" (none)")
|
||||
log.console("Dependencies for " + target_pkg + ":")
|
||||
print_deps(target_pkg)
|
||||
} else if (mode == 'shop') {
|
||||
log.console("Shop Packages:")
|
||||
var all = shop.list_packages()
|
||||
|
||||
if (all.length == 0)
|
||||
log.console(" (none)")
|
||||
else
|
||||
all.forEach(package => log.console(" " + package))
|
||||
}
|
||||
log.console("Shop packages:")
|
||||
log.console("")
|
||||
|
||||
function print_deps(ctx) {
|
||||
var deps = pkg.dependencies(ctx)
|
||||
var aliases = []
|
||||
for (var k in deps) aliases.push(k)
|
||||
aliases.sort()
|
||||
var packages = shop.list_packages()
|
||||
if (length(packages) == 0) {
|
||||
log.console(" (none)")
|
||||
} else {
|
||||
packages = sort(packages)
|
||||
|
||||
if (aliases.length == 0) {
|
||||
log.console(" (none)")
|
||||
} else {
|
||||
for (var i = 0; i < aliases.length; i++) {
|
||||
var alias = aliases[i]
|
||||
var locator = deps[alias]
|
||||
log.console(" " + alias + " -> " + locator)
|
||||
}
|
||||
// Group by type
|
||||
var local_pkgs = []
|
||||
var linked_pkgs = []
|
||||
var remote_pkgs = []
|
||||
|
||||
arrfor(packages, function(p) {
|
||||
if (p == 'core') return
|
||||
var lock_entry = lock[p]
|
||||
var link_target = links[p]
|
||||
|
||||
if (link_target) {
|
||||
push(linked_pkgs, p)
|
||||
} else if (lock_entry && lock_entry.type == 'local') {
|
||||
push(local_pkgs, p)
|
||||
} else {
|
||||
push(remote_pkgs, p)
|
||||
}
|
||||
})
|
||||
|
||||
if (length(linked_pkgs) > 0) {
|
||||
log.console("Linked packages:")
|
||||
arrfor(linked_pkgs, function(p) {
|
||||
var target = links[p]
|
||||
log.console(" " + p + " -> " + target)
|
||||
})
|
||||
log.console("")
|
||||
}
|
||||
|
||||
if (length(local_pkgs) > 0) {
|
||||
log.console("Local packages:")
|
||||
arrfor(local_pkgs, function(p) {
|
||||
log.console(" " + p)
|
||||
})
|
||||
log.console("")
|
||||
}
|
||||
|
||||
if (length(remote_pkgs) > 0) {
|
||||
log.console("Remote packages:")
|
||||
arrfor(remote_pkgs, function(p) {
|
||||
var lock_entry = lock[p]
|
||||
var commit = lock_entry && lock_entry.commit ? " @" + text(lock_entry.commit, 0, 8) : ""
|
||||
log.console(" " + p + commit)
|
||||
})
|
||||
log.console("")
|
||||
}
|
||||
|
||||
log.console("Total: " + text(length(packages)) + " package(s)")
|
||||
}
|
||||
}
|
||||
|
||||
$stop()
|
||||
|
||||
12
ls.ce
12
ls.ce
@@ -11,22 +11,22 @@ var modules = package.list_modules(pkg)
|
||||
var programs = package.list_programs(pkg)
|
||||
|
||||
log.console("Modules in " + pkg + ":")
|
||||
modules.sort()
|
||||
if (modules.length == 0) {
|
||||
modules = sort(modules)
|
||||
if (length(modules) == 0) {
|
||||
log.console(" (none)")
|
||||
} else {
|
||||
for (var i = 0; i < modules.length; i++) {
|
||||
for (var i = 0; i < length(modules); i++) {
|
||||
log.console(" " + modules[i])
|
||||
}
|
||||
}
|
||||
|
||||
log.console("")
|
||||
log.console("Programs in " + pkg + ":")
|
||||
programs.sort()
|
||||
if (programs.length == 0) {
|
||||
programs = sort(programs)
|
||||
if (length(programs) == 0) {
|
||||
log.console(" (none)")
|
||||
} else {
|
||||
for (var i = 0; i < programs.length; i++) {
|
||||
for (var i = 0; i < length(programs); i++) {
|
||||
log.console(" " + programs[i])
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user