new syntax for internals

This commit is contained in:
2026-02-10 11:03:01 -06:00
parent 6df3b741cf
commit 4deb0e2577
9 changed files with 483 additions and 292 deletions

View File

@@ -865,6 +865,7 @@ $_.clock(_ => {
var pkg = file_info ? file_info.package : null
env.use = function(path) { return shop.use(path, pkg) }
env.args = _cell.args.arg
env.log = log
var script = text(fd.slurp(prog_path))
var ast = analyze(script, prog_path)

Binary file not shown.

View File

@@ -35,7 +35,8 @@ function ensure_dir(path) {
if (fd.stat(path).isDirectory) return
var parts = array(path, '/')
var current = starts_with(path, '/') ? '/' : ''
for (var i = 0; i < length(parts); i++) {
var i = 0
for (i = 0; i < length(parts); i++) {
if (parts[i] == '') continue
current = current + parts[i] + '/'
if (!fd.stat(current).isDirectory) {
@@ -119,12 +120,16 @@ function split_explicit_package_import(path)
if (!looks_explicit) return null
// Find the longest prefix that is an installed package
for (var i = length(parts) - 1; i >= 1; i--) {
var pkg_candidate = text(array(parts, 0, i), '/')
var mod_path = text(array(parts, i), '/')
var i = 0
var pkg_candidate = null
var mod_path = null
var candidate_dir = null
for (i = length(parts) - 1; i >= 1; i--) {
pkg_candidate = text(array(parts, 0, i), '/')
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)
candidate_dir = get_packages_dir() + '/' + safe_package_path(pkg_candidate)
if (fd.is_file(candidate_dir + '/cell.toml'))
return {package: pkg_candidate, path: mod_path}
@@ -158,8 +163,9 @@ function abs_path_to_package(package_dir)
return 'core'
}
// Also check if core_dir is a symlink pointing to package_dir
var core_target = null
if (fd.is_link(core_dir)) {
var core_target = fd.readlink(core_dir)
core_target = fd.readlink(core_dir)
if (core_target == package_dir || fd.realpath(core_dir) == package_dir) {
return 'core'
}
@@ -181,9 +187,11 @@ function abs_path_to_package(package_dir)
// For local directories (e.g., linked targets), read the package name from cell.toml
var _toml_path = package_dir + '/cell.toml'
var content = null
var cfg = null
if (fd.is_file(_toml_path)) {
var content = text(fd.slurp(_toml_path))
var cfg = toml.decode(content)
content = text(fd.slurp(_toml_path))
cfg = toml.decode(content)
if (cfg.package)
return cfg.package
}
@@ -316,12 +324,16 @@ Shop.verify_package_name = function(pkg) {
// Convert module package to download URL
Shop.get_download_url = function(pkg, commit_hash) {
var info = Shop.resolve_package_info(pkg)
var parts = null
var host = null
var user = null
var repo = null
if (info == 'gitea') {
var parts = array(pkg, '/')
var host = parts[0]
var user = parts[1]
var repo = parts[2]
parts = array(pkg, '/')
host = parts[0]
user = parts[1]
repo = parts[2]
return 'https://' + host + '/' + user + '/' + repo + '/archive/' + commit_hash + '.zip'
}
@@ -332,12 +344,16 @@ Shop.get_download_url = function(pkg, commit_hash) {
// Get the API URL for checking remote git commits
Shop.get_api_url = function(pkg) {
var info = Shop.resolve_package_info(pkg)
var parts = null
var host = null
var user = null
var repo = null
if (info == 'gitea') {
var parts = array(pkg, '/')
var host = parts[0]
var user = parts[1]
var repo = parts[2]
parts = array(pkg, '/')
host = parts[0]
user = parts[1]
repo = parts[2]
return 'https://' + host + '/api/v1/repos/' + user + '/' + repo + '/branches/'
}
@@ -393,9 +409,12 @@ function inject_env(inject) {
}
// Add capability injections
for (var i = 0; i < length(inject); i++) {
var inj = inject[i]
var key = trim(inj, '$')
var i = 0
var inj = null
var key = null
for (i = 0; i < length(inject); i++) {
inj = inject[i]
key = trim(inj, '$')
if (key == 'fd') env[key] = fd
else env[key] = my$_[key]
}
@@ -409,15 +428,19 @@ function inject_bindings_code(inject) {
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]
var i = 0
var fn = null
var inj = null
var key = null
for (i = 0; i < length(runtime_fns); i++) {
fn = runtime_fns[i]
push(lines, `var ${fn} = env["${fn}"];`)
}
// Capability bindings ($delay, $start, etc.)
for (var i = 0; i < length(inject); i++) {
var inj = inject[i]
var key = trim(inj, '$')
for (i = 0; i < length(inject); i++) {
inj = inject[i]
key = trim(inj, '$')
push(lines, `var $${key} = env["${key}"];`)
}
return text(lines, '\n')
@@ -474,26 +497,34 @@ function resolve_mod_fn(path, pkg) {
// given a path and a package context
// return module info about where it was found
function resolve_locator(path, ctx)
{
{
var explicit = split_explicit_package_import(path)
var explicit_path = null
var fn = null
var core_dir = null
var core_file_path = null
var is_core = null
var scope = null
var alias_path = null
if (explicit) {
if (is_internal_path(explicit.path) && ctx && explicit.package != ctx)
explicit = null
}
if (explicit) {
var explicit_path = get_packages_dir() + '/' + safe_package_path(explicit.package) + '/' + explicit.path
explicit_path = get_packages_dir() + '/' + safe_package_path(explicit.package) + '/' + explicit.path
if (fd.is_file(explicit_path)) {
var fn = resolve_mod_fn(explicit_path, explicit.package)
fn = resolve_mod_fn(explicit_path, explicit.package)
return {path: explicit_path, scope: SCOPE_PACKAGE, symbol: fn}
}
}
// 1. If no context, resolve from core only
if (!ctx) {
var core_dir = Shop.get_core_dir()
var core_file_path = core_dir + '/' + path
core_dir = Shop.get_core_dir()
core_file_path = core_dir + '/' + path
if (fd.is_file(core_file_path)) {
var fn = resolve_mod_fn(core_file_path, 'core')
fn = resolve_mod_fn(core_file_path, 'core')
return {path: core_file_path, scope: SCOPE_CORE, symbol: fn}
}
return null
@@ -502,7 +533,7 @@ function resolve_locator(path, ctx)
// check in ctx package
// If ctx is an absolute path (starts with /), use it directly
// Otherwise, look it up in the packages directory
var ctx_dir
var ctx_dir = null
if (starts_with(ctx, '/')) {
ctx_dir = ctx
} else {
@@ -511,10 +542,10 @@ function resolve_locator(path, ctx)
var ctx_path = ctx_dir + '/' + path
if (fd.is_file(ctx_path)) {
var fn = resolve_mod_fn(ctx_path, ctx)
fn = resolve_mod_fn(ctx_path, ctx)
// Check if ctx is the core package (either by name or by path)
var is_core = (ctx == 'core') || (ctx_dir == Shop.get_core_dir())
var scope = is_core ? SCOPE_CORE : SCOPE_LOCAL
is_core = (ctx == 'core') || (ctx_dir == Shop.get_core_dir())
scope = is_core ? SCOPE_CORE : SCOPE_LOCAL
return {path: ctx_path, scope: scope, symbol: fn}
}
@@ -524,24 +555,24 @@ function resolve_locator(path, ctx)
// check for aliased dependency
var alias = pkg_tools.split_alias(ctx, path)
if (alias) {
var alias_path = get_packages_dir() + '/' + safe_package_path(alias.package) + '/' + alias.path
alias_path = get_packages_dir() + '/' + safe_package_path(alias.package) + '/' + alias.path
if (fd.is_file(alias_path)) {
var fn = resolve_mod_fn(alias_path, ctx)
fn = resolve_mod_fn(alias_path, ctx)
return {path: alias_path, scope:SCOPE_PACKAGE, symbol:fn}
}
}
var package_path = get_packages_dir() + '/' + safe_package_path(path)
var package_path = get_packages_dir() + '/' + safe_package_path(path)
if (fd.is_file(package_path)) {
var fn = resolve_mod_fn(package_path, ctx)
fn = resolve_mod_fn(package_path, ctx)
return {path: package_path, scope: SCOPE_PACKAGE, symbol: fn}
}
// 4. Check core as fallback
var core_dir = Shop.get_core_dir()
var core_file_path = core_dir + '/' + path
core_dir = Shop.get_core_dir()
core_file_path = core_dir + '/' + path
if (fd.is_file(core_file_path)) {
var fn = resolve_mod_fn(core_file_path, 'core')
fn = resolve_mod_fn(core_file_path, 'core')
return {path: core_file_path, scope: SCOPE_CORE, symbol: fn}
}
@@ -573,7 +604,7 @@ Shop.open_package_dylib = function(pkg) {
var link_target = link.get_target(pkg)
var resolved_pkg = link_target ? link_target : pkg
var pkg_dir;
var pkg_dir = null
if (starts_with(resolved_pkg, '/')) {
pkg_dir = resolved_pkg
} else {
@@ -581,9 +612,11 @@ Shop.open_package_dylib = function(pkg) {
}
var toml_path = pkg_dir + '/cell.toml'
var content = null
var cfg = null
if (fd.is_file(toml_path)) {
var content = text(fd.slurp(toml_path))
var cfg = toml.decode(content)
content = text(fd.slurp(toml_path))
cfg = toml.decode(content)
if (cfg.dependencies) {
arrfor(array(cfg.dependencies), function(alias, i) {
var dep_pkg = cfg.dependencies[alias]
@@ -606,12 +639,19 @@ Shop.open_package_dylib = function(pkg) {
// Core is never loaded as a dynamic library via dlopen
function resolve_c_symbol(path, package_context) {
var explicit = split_explicit_package_import(path)
var sym = null
var dl_path = null
var _path = null
var core_sym = null
var canon_pkg = null
var mod_name = null
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)
sym = make_c_symbol(explicit.package, explicit.path)
if (os.internal_exists(sym)) {
return {
symbol: function() { return os.load_internal(sym) },
@@ -622,7 +662,7 @@ function resolve_c_symbol(path, package_context) {
}
Shop.open_package_dylib(explicit.package)
var dl_path = get_lib_path(explicit.package)
dl_path = get_lib_path(explicit.package)
if (open_dls[dl_path] && os.dylib_has_symbol(open_dls[dl_path], sym)) {
return {
symbol: function() { return os.dylib_symbol(open_dls[dl_path], sym) },
@@ -635,8 +675,8 @@ function resolve_c_symbol(path, package_context) {
// If no package context, only check core internal symbols
if (!package_context || package_context == 'core') {
var _path = replace(path, '/', '_')
var core_sym = `js_${_path}_use`
_path = replace(path, '/', '_')
core_sym = `js_${_path}_use`
if (os.internal_exists(core_sym)) {
return {
symbol: function() { return os.load_internal(core_sym) },
@@ -648,7 +688,7 @@ function resolve_c_symbol(path, package_context) {
}
// 1. Check own package first (internal, then dylib)
var sym = make_c_symbol(package_context, path)
sym = make_c_symbol(package_context, path)
if (os.internal_exists(sym)) {
return {
symbol: function() { return os.load_internal(sym) },
@@ -658,7 +698,7 @@ function resolve_c_symbol(path, package_context) {
}
Shop.open_package_dylib(package_context)
var dl_path = get_lib_path(package_context)
dl_path = get_lib_path(package_context)
if (open_dls[dl_path] && os.dylib_has_symbol(open_dls[dl_path], sym)) {
return {
@@ -674,10 +714,10 @@ function resolve_c_symbol(path, package_context) {
// 2. Check aliased package imports (e.g. 'prosperon/sprite')
var pkg_alias = get_import_package(path)
if (pkg_alias) {
var canon_pkg = get_aliased_package(path, package_context)
canon_pkg = get_aliased_package(path, package_context)
if (canon_pkg) {
var mod_name = get_import_name(path)
var sym = make_c_symbol(canon_pkg, mod_name)
mod_name = get_import_name(path)
sym = make_c_symbol(canon_pkg, mod_name)
// Check internal first
if (os.internal_exists(sym)) {
@@ -691,7 +731,7 @@ function resolve_c_symbol(path, package_context) {
// Then check dylib
Shop.open_package_dylib(canon_pkg)
var dl_path = get_lib_path(canon_pkg)
dl_path = get_lib_path(canon_pkg)
if (open_dls[dl_path] && os.dylib_has_symbol(open_dls[dl_path], sym)) {
return {
symbol: function() { return os.dylib_symbol(open_dls[dl_path], sym) },
@@ -704,7 +744,7 @@ function resolve_c_symbol(path, package_context) {
}
// 3. Check core internal symbols (core is never a dynamic library)
var core_sym = `js_${replace(path, '/', '_')}_use`
core_sym = `js_${replace(path, '/', '_')}_use`
if (os.internal_exists(core_sym)) {
return {
symbol: function() { return os.load_internal(core_sym) },
@@ -732,31 +772,37 @@ function resolve_module_info(path, package_context) {
if (min_scope == 999)
return null
var cache_key
var cache_key = null
var real_path = null
var real_info = null
var pkg_alias = null
var canon_pkg = null
var mod_name = null
if (mod_resolve.scope == SCOPE_CORE) {
cache_key = 'core/' + path
} else if (mod_resolve.scope < 900 && mod_resolve.path) {
var real_path = fd.realpath(mod_resolve.path)
real_path = fd.realpath(mod_resolve.path)
if (real_path) {
var real_info = Shop.file_info(real_path)
real_info = Shop.file_info(real_path)
if (real_info.package && real_info.name)
cache_key = real_info.package + '/' + real_info.name
else
cache_key = real_path
}
}
if (!cache_key) {
if (min_scope == SCOPE_CORE)
cache_key = 'core/' + path
else if (min_scope == SCOPE_LOCAL && package_context)
cache_key = package_context + '/' + path
else if (min_scope == SCOPE_PACKAGE) {
var pkg_alias = get_import_package(path)
pkg_alias = get_import_package(path)
if (pkg_alias) {
var canon_pkg = get_canonical_package(pkg_alias, package_context)
canon_pkg = get_canonical_package(pkg_alias, package_context)
if (canon_pkg) {
var mod_name = get_import_name(path)
mod_name = get_import_name(path)
cache_key = canon_pkg + '/' + mod_name
} else
cache_key = path
@@ -807,20 +853,26 @@ function execute_module(info)
var c_resolve = info.c_resolve
var mod_resolve = info.mod_resolve
var used
var used = null
var context = null
var file_info = null
var inject = null
var env = null
var pkg = null
var use_fn = null
if (mod_resolve.scope < 900) {
var context = null
context = null
if (c_resolve.scope < 900) {
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 env = inject_env(inject)
var pkg = file_info.package
var use_fn = make_use_fn(pkg)
file_info = Shop.file_info(mod_resolve.path)
inject = Shop.script_inject_for(file_info)
env = inject_env(inject)
pkg = file_info.package
use_fn = make_use_fn(pkg)
// Call with signature: setup_module(args, use, env)
// args is null for module loading
@@ -942,17 +994,18 @@ Shop.fetch = function(pkg) {
// Check if we have the zip cached
var zip_blob = get_cached_zip(pkg, commit)
var actual_hash = null
if (zip_blob) {
// If we have a hash on record, verify it
if (expected_hash) {
var actual_hash = text(crypto.blake2(zip_blob), 'h')
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')
actual_hash = text(crypto.blake2(zip_blob), 'h')
lock_entry.zip_hash = actual_hash
Shop.save_lock(lock)
return { status: 'cached' }
@@ -1004,10 +1057,12 @@ Shop.extract = function(pkg) {
// Check if already extracted at correct commit
var lock = Shop.load_lock()
var lock_entry = lock[pkg]
var extracted_commit_file = null
var extracted_commit = null
if (lock_entry && lock_entry.commit) {
var extracted_commit_file = target_dir + '/.cell_commit'
extracted_commit_file = target_dir + '/.cell_commit'
if (fd.is_file(extracted_commit_file)) {
var extracted_commit = trim(text(fd.slurp(extracted_commit_file)))
extracted_commit = trim(text(fd.slurp(extracted_commit_file)))
if (extracted_commit == lock_entry.commit) {
// Already extracted at this commit, skip
return true
@@ -1059,6 +1114,7 @@ Shop.update = function(pkg) {
log.console(`checking ${pkg}`)
var new_entry = null
if (info == 'local') {
// Check if local path exists
if (!fd.is_dir(pkg)) {
@@ -1066,7 +1122,7 @@ Shop.update = function(pkg) {
return null
}
// Local packages always get a lock entry
var new_entry = {
new_entry = {
type: 'local',
updated: time.number()
}
@@ -1089,7 +1145,7 @@ Shop.update = function(pkg) {
if (local_commit == remote_commit)
return null
var new_entry = {
new_entry = {
type: info,
commit: remote_commit,
updated: time.number()
@@ -1114,21 +1170,28 @@ function install_zip(zip_blob, target_dir) {
var count = zip.count()
var created_dirs = {}
for (var i = 0; i < count; i++) {
var i = 0
var filename = null
var slash_pos = null
var rel_path = null
var full_path = null
var dir_path = null
var file_data = null
for (i = 0; i < count; i++) {
if (zip.is_directory(i)) continue
var filename = zip.get_filename(i)
var slash_pos = search(filename, '/')
filename = zip.get_filename(i)
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 = fd.dirname(full_path)
rel_path = text(filename, slash_pos + 1)
full_path = target_dir + '/' + rel_path
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)
file_data = zip.slurp(filename)
stone(file_data)
@@ -1151,14 +1214,16 @@ Shop.remove = function(pkg) {
Shop.get = function(pkg) {
var lock = Shop.load_lock()
var info = null
var commit = null
if (!lock[pkg]) {
var info = Shop.resolve_package_info(pkg)
info = Shop.resolve_package_info(pkg)
if (!info) {
print("Invalid package: " + pkg); disrupt
}
var commit = null
commit = null
if (info != 'local') {
commit = fetch_remote_hash(pkg)
if (!commit) {
@@ -1216,9 +1281,11 @@ function get_package_scripts(package)
{
var files = pkg_tools.list_files(package)
var scripts = []
for (var i = 0; i < length(files); i++) {
var file = files[i]
var i = 0
var file = null
for (i = 0; i < length(files); i++) {
file = files[i]
if (ends_with(file, '.cm') || ends_with(file, '.ce')) {
push(scripts, file)
}

View File

@@ -4,19 +4,21 @@ var pkg = use('package')
// Check if current directory is a valid cell package
function is_valid_package(dir) {
if (!dir) dir = '.'
return fd.is_file(dir + '/cell.toml')
var _dir = dir == null ? '.' : dir
if (!_dir) _dir = '.'
return fd.is_file(_dir + '/cell.toml')
}
// Get current package name from cell.toml or null
function get_current_package_name() {
if (!is_valid_package('.')) return null
try {
var _load = function() {
var config = pkg.load_config(null)
return config.package || 'local'
} catch (e) {
} disruption {
return 'local'
}
return _load()
}
// Get the directory for a package
@@ -37,9 +39,10 @@ function ensure_dir(path) {
var parts = array(path, '/')
var current = starts_with(path, '/') ? '/' : ''
for (var i = 0; i < length(parts); i++) {
var i = 0
for (i = 0; i < length(parts); i++) {
if (parts[i] == '') continue
current += parts[i] + '/'
current = current + parts[i] + '/'
if (!fd.is_dir(current)) {
fd.mkdir(current)
}

View File

@@ -34,7 +34,8 @@ function ensure_dir(path) {
if (fd.stat(path).isDirectory) return
var parts = array(path, '/')
var current = starts_with(path, '/') ? '/' : ''
for (var i = 0; i < length(parts); i++) {
var i = 0
for (i = 0; i < length(parts); i++) {
if (parts[i] == '') continue
current = current + parts[i] + '/'
if (!fd.stat(current).isDirectory) {
@@ -119,8 +120,9 @@ Link.add = function(canonical, target, shop) {
// 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'
var _install_deps = null
if (fd.is_file(toml_path)) {
var _install_deps = function() {
_install_deps = function() {
var content = text(fd.slurp(toml_path))
var cfg = toml.decode(content)
if (cfg && cfg.dependencies) {

View File

@@ -101,11 +101,12 @@ package.alias_to_package = function(name, alias)
}
// alias is optional
package.add_dependency = function(name, locator, alias = locator)
package.add_dependency = function(name, locator, alias)
{
var _alias = alias == null ? locator : alias
var config = package.load_config(name)
if (!config.dependencies) config.dependencies = {}
config.dependencies[alias] = locator
config.dependencies[_alias] = locator
package.save_config(name, config)
}
@@ -115,10 +116,11 @@ package.remove_dependency = function(name, locator)
var config = package.load_config(name)
if (!config.dependencies) return
var alias = null
if (config.dependencies[locator])
delete config.dependencies[locator]
else {
var alias = package.find_alias(name, locator)
alias = package.find_alias(name, locator)
if (alias)
delete config.dependencies[alias]
}
@@ -133,8 +135,9 @@ package.find_package_dir = function(file)
if (fd.is_file(dir))
dir = fd.dirname(dir)
var toml_path = null
while (dir && length(dir) > 0) {
var toml_path = dir + '/cell.toml'
toml_path = dir + '/cell.toml'
if (fd.is_file(toml_path)) {
return dir
}
@@ -163,9 +166,11 @@ package.split_alias = function(name, path)
if (!config) return null
var deps = config.dependencies
var dep_locator = null
var remaining_path = null
if (deps && deps[first_part]) {
var dep_locator = deps[first_part]
var remaining_path = text(array(parts, 1), '/')
dep_locator = deps[first_part]
remaining_path = text(array(parts, 1), '/')
return { package: dep_locator, path: remaining_path }
}
return null
@@ -208,18 +213,23 @@ package.list_files = function(pkg) {
var walk = function(current_dir, current_prefix) {
var list = fd.readdir(current_dir)
if (!list) return
for (var i = 0; i < length(list); i++) {
var item = list[i]
var i = 0
var item = null
var full_path = null
var rel_path = null
var st = null
for (i = 0; i < length(list); i++) {
item = list[i]
if (item == '.' || item == '..') continue
if (starts_with(item, '.')) continue
if (starts_with(item, '.')) continue
// Skip build directories in root
var full_path = current_dir + "/" + item
var rel_path = current_prefix ? current_prefix + "/" + item : item
var st = fd.stat(full_path)
full_path = current_dir + "/" + item
rel_path = current_prefix ? current_prefix + "/" + item : item
st = fd.stat(full_path)
if (st.isDirectory) {
walk(full_path, rel_path)
} else {
@@ -237,7 +247,8 @@ package.list_files = function(pkg) {
package.list_modules = function(name) {
var files = package.list_files(name)
var modules = []
for (var i = 0; i < length(files); i++) {
var i = 0
for (i = 0; i < length(files); i++) {
if (ends_with(files[i], '.cm')) {
push(modules, text(files[i], 0, -3))
}
@@ -248,7 +259,8 @@ package.list_modules = function(name) {
package.list_programs = function(name) {
var files = package.list_files(name)
var programs = []
for (var i = 0; i < length(files); i++) {
var i = 0
for (i = 0; i < length(files); i++) {
if (ends_with(files[i], '.ce')) {
push(programs, text(files[i], 0, -3))
}
@@ -265,14 +277,16 @@ package.get_flags = function(name, flag_type, target) {
var flags = []
// Base flags
var base = null
var target_flags = null
if (config.compilation && config.compilation[flag_type]) {
var base = config.compilation[flag_type]
base = config.compilation[flag_type]
flags = array(flags, filter(array(base, /\s+/), function(f) { return length(f) > 0 }))
}
// Target-specific flags
if (target && config.compilation && config.compilation[target] && config.compilation[target][flag_type]) {
var target_flags = config.compilation[target][flag_type]
target_flags = config.compilation[target][flag_type]
flags = array(flags, filter(array(target_flags, /\s+/), function(f) { return length(f) > 0 }))
}
@@ -290,23 +304,36 @@ package.get_c_files = function(name, target, exclude_main) {
// Group files by their base name (without target suffix)
var groups = {} // base_key -> { generic: file, variants: { target: file } }
for (var i = 0; i < length(files); i++) {
var file = files[i]
var i = 0
var file = null
var ext = null
var base = null
var name_part = null
var dir_part = null
var dir = null
var is_variant = null
var variant_target = null
var generic_name = null
var t = 0
var suffix = null
var group_key = null
for (i = 0; i < length(files); i++) {
file = files[i]
if (!ends_with(file, '.c') && !ends_with(file, '.cpp')) continue
var ext = ends_with(file, '.cpp') ? '.cpp' : '.c'
var base = text(file, 0, -length(ext))
var name_part = fd.basename(base)
var dir_part = fd.dirname(base)
var dir = (dir_part && dir_part != '.') ? dir_part + '/' : ''
ext = ends_with(file, '.cpp') ? '.cpp' : '.c'
base = text(file, 0, -length(ext))
name_part = fd.basename(base)
dir_part = fd.dirname(base)
dir = (dir_part && dir_part != '.') ? dir_part + '/' : ''
// Check for target suffix
var is_variant = false
var variant_target = null
var generic_name = name_part
for (var t = 0; t < length(known_targets); t++) {
var suffix = '_' + known_targets[t]
is_variant = false
variant_target = null
generic_name = name_part
for (t = 0; t < length(known_targets); t++) {
suffix = '_' + known_targets[t]
if (ends_with(name_part, suffix)) {
is_variant = true
variant_target = known_targets[t]
@@ -315,7 +342,7 @@ package.get_c_files = function(name, target, exclude_main) {
}
}
var group_key = dir + generic_name + ext
group_key = dir + generic_name + ext
if (!groups[group_key]) {
groups[group_key] = { generic: null, variants: {} }
}
@@ -332,18 +359,19 @@ package.get_c_files = function(name, target, exclude_main) {
arrfor(array(groups), function(key) {
var group = groups[key]
var selected = null
var basename = null
// Prefer target-specific variant if available
if (target && group.variants[target]) {
selected = group.variants[target]
} else if (group.generic) {
selected = group.generic
}
if (selected) {
// Skip main.c if requested
if (exclude_main) {
var basename = fd.basename(selected)
basename = fd.basename(selected)
if (basename == 'main.c' || starts_with(basename, 'main_')) return
}
push(result, selected)

301
test.ce
View File

@@ -8,7 +8,7 @@ var dbg = use('js')
// run gc with dbg.gc()
if (!args) args = []
var _args = args == null ? [] : args
var target_pkg = null // null = current package
var target_test = null // null = all tests, otherwise specific test file
@@ -22,19 +22,20 @@ var actor_test_results = []
// Check if current directory is a valid cell package
function is_valid_package(dir) {
if (!dir) dir = '.'
return fd.is_file(dir + '/cell.toml')
var _dir = dir == null ? '.' : dir
return fd.is_file(_dir + '/cell.toml')
}
// Get current package name from cell.toml or null
function get_current_package_name() {
if (!is_valid_package('.')) return null
try {
var _load = function() {
var config = pkg.load_config(null)
return config.package || 'local'
} catch (e) {
} disruption {
return 'local'
}
return _load()
}
// Parse arguments
@@ -48,16 +49,21 @@ function get_current_package_name() {
function parse_args() {
var cleaned_args = []
for (var i = 0; i < length(args); i++) {
if (args[i] == '-g') {
var i = 0
var name = null
var lock = null
var resolved = null
var test_path = null
for (i = 0; i < length(_args); i++) {
if (_args[i] == '-g') {
gc_after_each_test = true
} else {
push(cleaned_args, args[i])
push(cleaned_args, _args[i])
}
}
args = cleaned_args
_args = cleaned_args
if (length(args) == 0) {
if (length(_args) == 0) {
// cell test - run all tests for current package
if (!is_valid_package('.')) {
log.console('No cell.toml found in current directory')
@@ -67,7 +73,7 @@ function parse_args() {
return true
}
if (args[0] == 'all') {
if (_args[0] == 'all') {
// cell test all - run all tests for current package
if (!is_valid_package('.')) {
log.console('No cell.toml found in current directory')
@@ -77,14 +83,14 @@ function parse_args() {
return true
}
if (args[0] == 'package') {
if (length(args) < 2) {
if (_args[0] == 'package') {
if (length(_args) < 2) {
log.console('Usage: cell test package <name> [test]')
log.console(' cell test package all')
return false
}
if (args[1] == 'all') {
if (_args[1] == 'all') {
// cell test package all - run tests from all packages
all_pkgs = true
log.console('Testing all packages...')
@@ -92,18 +98,19 @@ function parse_args() {
}
// cell test package <name> [test]
var name = args[1]
name = _args[1]
// Check if package exists in lock or is a local path
var lock = shop.load_lock()
lock = shop.load_lock()
if (lock[name]) {
target_pkg = name
} else if (starts_with(name, '/') && is_valid_package(name)) {
target_pkg = name
} else {
// Try to resolve as dependency alias from current package
resolved = null
if (is_valid_package('.')) {
var resolved = pkg.alias_to_package(null, name)
resolved = pkg.alias_to_package(null, name)
if (resolved) {
target_pkg = resolved
} else {
@@ -116,9 +123,9 @@ function parse_args() {
}
}
if (length(args) >= 3) {
if (length(_args) >= 3) {
// cell test package <name> <test>
target_test = args[2]
target_test = _args[2]
}
log.console(`Testing package: ${target_pkg}`)
@@ -126,7 +133,7 @@ function parse_args() {
}
// cell test tests/suite or cell test <path> - specific test file
var test_path = args[0]
test_path = _args[0]
// Normalize path - add tests/ prefix if not present and doesn't start with /
if (!starts_with(test_path, 'tests/') && !starts_with(test_path, '/')) {
@@ -160,9 +167,10 @@ function ensure_dir(path) {
var parts = array(path, '/')
var current = starts_with(path, '/') ? '/' : ''
for (var i = 0; i < length(parts); i++) {
var i = 0
for (i = 0; i < length(parts); i++) {
if (parts[i] == '') continue
current += parts[i] + '/'
current = current + parts[i] + '/'
if (!fd.is_dir(current))
fd.mkdir(current)
}
@@ -189,23 +197,29 @@ function collect_actor_tests(package_name, specific_test) {
var files = pkg.list_files(package_name)
var actor_tests = []
for (var i = 0; i < length(files); i++) {
var f = files[i]
var i = 0
var f = null
var test_name = null
var match_name = null
var test_base = null
var match_base = null
for (i = 0; i < length(files); i++) {
f = files[i]
// Check if file is in tests/ folder and is a .ce actor
if (starts_with(f, "tests/") && ends_with(f, ".ce")) {
// If specific test requested, filter
if (specific_test) {
var test_name = text(f, 0, -3) // remove .ce
var match_name = specific_test
test_name = text(f, 0, -3) // remove .ce
match_name = specific_test
if (!starts_with(match_name, 'tests/')) match_name = 'tests/' + match_name
if (!ends_with(match_name, '.ce')) match_name = match_name
// Match without extension
var test_base = test_name
var match_base = ends_with(match_name, '.ce') ? text(match_name, 0, -3) : match_name
test_base = test_name
match_base = ends_with(match_name, '.ce') ? text(match_name, 0, -3) : match_name
if (test_base != match_base) continue
}
push(actor_tests,{
push(actor_tests, {
package: package_name || "local",
file: f,
path: prefix + '/' + f
@@ -229,19 +243,19 @@ function spawn_actor_test(test_info) {
actor: null
}
try {
// Spawn the actor test - it should send back results
var _spawn = function() {
var actor_path = text(test_info.path, 0, -3) // remove .ce
entry.actor = $start(actor_path)
push(pending_actor_tests, entry)
} catch (e) {
} disruption {
entry.status = "failed"
entry.error = { message: `Failed to spawn actor: ${e}` }
entry.error = { message: `Failed to spawn actor` }
entry.duration_ns = 0
push(actor_test_results, entry)
log.console(` FAIL ${test_name}: `)
log.error(e)
log.error()
}
_spawn()
}
function run_tests(package_name, specific_test) {
@@ -260,17 +274,24 @@ function run_tests(package_name, specific_test) {
var files = pkg.list_files(package_name)
var test_files = []
for (var i = 0; i < length(files); i++) {
var f = files[i]
var i = 0
var f = null
var test_name = null
var match_name = null
var match_base = null
var mod_path = null
var file_result = null
for (i = 0; i < length(files); i++) {
f = files[i]
// Check if file is in tests/ folder and is a .cm module (not .ce - those are actor tests)
if (starts_with(f, "tests/") && ends_with(f, ".cm")) {
// If specific test requested, filter
if (specific_test) {
var test_name = text(f, 0, -3) // remove .cm
var match_name = specific_test
test_name = text(f, 0, -3) // remove .cm
match_name = specific_test
if (!starts_with(match_name, 'tests/')) match_name = 'tests/' + match_name
// Match without extension
var match_base = ends_with(match_name, '.cm') ? text(match_name, 0, -3) : match_name
match_base = ends_with(match_name, '.cm') ? text(match_name, 0, -3) : match_name
if (test_name != match_base) continue
}
push(test_files, f)
@@ -282,24 +303,31 @@ function run_tests(package_name, specific_test) {
else log.console(`Running tests for local package`)
}
for (var i = 0; i < length(test_files); i++) {
var f = test_files[i]
var mod_path = text(f, 0, -3) // remove .cm
var _load_file = null
for (i = 0; i < length(test_files); i++) {
f = test_files[i]
mod_path = text(f, 0, -3) // remove .cm
var file_result = {
file_result = {
name: f,
tests: [],
passed: 0,
failed: 0
}
try {
var test_mod
// For local packages (null), use the current directory as package context
_load_file = function() {
var test_mod = null
var use_pkg = package_name ? package_name : fd.realpath('.')
test_mod = shop.use(mod_path, use_pkg)
var tests = []
var j = 0
var t = null
var test_entry = null
var start_time = null
var _test_error = null
var end_time = null
var _run_one = null
if (is_function(test_mod)) {
push(tests, {name: 'main', fn: test_mod})
} else if (is_object(test_mod)) {
@@ -312,50 +340,55 @@ function run_tests(package_name, specific_test) {
if (length(tests) > 0) {
log.console(` ${f}`)
for (var j = 0; j < length(tests); j++) {
var t = tests[j]
var test_entry = {
for (j = 0; j < length(tests); j++) {
t = tests[j]
test_entry = {
package: pkg_result.package,
test: t.name,
status: "pending",
duration_ns: 0
}
var start_time = time.number()
try {
start_time = time.number()
_test_error = null
_run_one = function() {
var ret = t.fn()
if (is_text(ret)) {
throw Error(ret)
_test_error = Error(ret)
disrupt
} else if (ret && (is_text(ret.message) || is_proto(ret, Error))) {
throw ret
_test_error = ret
disrupt
}
test_entry.status = "passed"
log.console(` PASS ${t.name}`)
pkg_result.passed++
file_result.passed++
} catch (e) {
} disruption {
var e = _test_error
test_entry.status = "failed"
test_entry.error = {
message: e,
stack: e.stack || ""
stack: (e && e.stack) ? e.stack : ""
}
if (e.name) test_entry.error.name = e.name
if (e && e.name) test_entry.error.name = e.name
if (is_object(e) && e.message) {
test_entry.error.message = e.message
test_entry.error.message = e.message
}
log.console(` FAIL ${t.name} ${test_entry.error.message}`)
if (test_entry.error.stack) {
log.console(` ${text(array(test_entry.error.stack, '\n'), '\n ')}`)
log.console(` ${text(array(test_entry.error.stack, '\n'), '\n ')}`)
}
pkg_result.failed++
file_result.failed++
}
var end_time = time.number()
_run_one()
end_time = time.number()
test_entry.duration_ns = round((end_time - start_time) * 1000000000)
push(file_result.tests, test_entry)
@@ -366,15 +399,15 @@ function run_tests(package_name, specific_test) {
}
}
} catch (e) {
log.console(` Error loading ${f}: ${e}`)
var test_entry = {
} disruption {
var test_entry = {
package: pkg_result.package,
test: "load_module",
status: "failed",
duration_ns: 0,
error: { message: `Error loading module: ${e}` }
error: { message: `Error loading module` }
}
log.console(` Error loading ${f}`)
push(file_result.tests, test_entry)
pkg_result.failed++
file_result.failed++
@@ -383,6 +416,7 @@ function run_tests(package_name, specific_test) {
dbg.gc()
}
}
_load_file()
push(pkg_result.files, file_result)
}
return pkg_result
@@ -390,6 +424,8 @@ function run_tests(package_name, specific_test) {
var all_results = []
var all_actor_tests = []
var packages = null
var i = 0
if (all_pkgs) {
// Run local first if we're in a valid package
@@ -399,8 +435,8 @@ if (all_pkgs) {
}
// Then all packages in lock
var packages = shop.list_packages()
for (var i = 0; i < length(packages); i++) {
packages = shop.list_packages()
for (i = 0; i < length(packages); i++) {
push(all_results, run_tests(packages[i], null))
all_actor_tests = array(all_actor_tests, collect_actor_tests(packages[i], null))
}
@@ -412,7 +448,7 @@ if (all_pkgs) {
// Spawn actor tests if any
if (length(all_actor_tests) > 0) {
log.console(`Running ${length(all_actor_tests)} actor test(s)...`)
for (var i = 0; i < length(all_actor_tests); i++) {
for (i = 0; i < length(all_actor_tests); i++) {
spawn_actor_test(all_actor_tests[i])
}
}
@@ -421,7 +457,10 @@ if (length(all_actor_tests) > 0) {
function handle_actor_message(msg) {
var sender = msg.$sender
var found_idx = -1
for (var i = 0; i < length(pending_actor_tests); i++) {
var i = 0
var res = null
var entry = null
for (i = 0; i < length(pending_actor_tests); i++) {
if (pending_actor_tests[i].actor == sender) {
found_idx = i
break
@@ -445,9 +484,9 @@ function handle_actor_message(msg) {
results = [msg]
}
for (var i = 0; i < length(results); i++) {
var res = results[i] || {}
var entry = {
for (i = 0; i < length(results); i++) {
res = results[i] || {}
entry = {
package: base_entry.package,
file: base_entry.file,
test: res.test || base_entry.test + (length(results) > 1 ? `#${i+1}` : ""),
@@ -481,18 +520,22 @@ function handle_actor_message(msg) {
function check_timeouts() {
var now = time.number()
var timed_out = []
var i = 0
var entry = null
var elapsed_ms = null
var idx = null
for (var i = length(pending_actor_tests) - 1; i >= 0; i--) {
var entry = pending_actor_tests[i]
var elapsed_ms = (now - entry.start_time) * 1000
for (i = length(pending_actor_tests) - 1; i >= 0; i--) {
entry = pending_actor_tests[i]
elapsed_ms = (now - entry.start_time) * 1000
if (elapsed_ms > ACTOR_TEST_TIMEOUT) {
push(timed_out, i)
}
}
for (var i = 0; i < length(timed_out); i++) {
var idx = timed_out[i]
var entry = pending_actor_tests[idx]
for (i = 0; i < length(timed_out); i++) {
idx = timed_out[i]
entry = pending_actor_tests[idx]
pending_actor_tests = array(array(pending_actor_tests, 0, idx), array(pending_actor_tests, idx + 1))
entry.status = "failed"
@@ -519,11 +562,17 @@ function check_completion() {
}
function finalize_results() {
var i = 0
var j = 0
var r = null
var pkg_result = null
var file_result = null
// Add actor test results to all_results
for (var i = 0; i < length(actor_test_results); i++) {
var r = actor_test_results[i]
var pkg_result = null
for (var j = 0; j < length(all_results); j++) {
for (i = 0; i < length(actor_test_results); i++) {
r = actor_test_results[i]
pkg_result = null
for (j = 0; j < length(all_results); j++) {
if (all_results[j].package == r.package) {
pkg_result = all_results[j]
break
@@ -534,8 +583,8 @@ function finalize_results() {
push(all_results, pkg_result)
}
var file_result = null
for (var j = 0; j < length(pkg_result.files); j++) {
file_result = null
for (j = 0; j < length(pkg_result.files); j++) {
if (pkg_result.files[j].name == r.file) {
file_result = pkg_result.files[j]
break
@@ -559,7 +608,7 @@ function finalize_results() {
// Calculate totals
var totals = { total: 0, passed: 0, failed: 0 }
for (var i = 0; i < length(all_results); i++) {
for (i = 0; i < length(all_results); i++) {
totals.total += all_results[i].total
totals.passed += all_results[i].passed
totals.failed += all_results[i].failed
@@ -573,10 +622,10 @@ function finalize_results() {
}
// If no actor tests, finalize immediately
var totals
var totals = null
if (length(all_actor_tests) == 0) {
totals = { total: 0, passed: 0, failed: 0 }
for (var i = 0; i < length(all_results); i++) {
for (i = 0; i < length(all_results); i++) {
totals.total += all_results[i].total
totals.passed += all_results[i].passed
totals.failed += all_results[i].failed
@@ -593,6 +642,16 @@ function generate_reports(totals) {
var timestamp = text(floor(time.number()))
var report_dir = shop.get_reports_dir() + '/test_' + timestamp
ensure_dir(report_dir)
var i = 0
var j = 0
var k = 0
var pkg_res = null
var f = null
var status = null
var t = null
var dur = null
var pkg_tests = null
var json_path = null
var txt_report = `TEST REPORT
Date: ${time.text(time.number())}
@@ -600,53 +659,53 @@ Total: ${totals.total}, Passed: ${totals.passed}, Failed: ${totals.failed}
=== SUMMARY ===
`
for (var i = 0; i < length(all_results); i++) {
var pkg_res = all_results[i]
for (i = 0; i < length(all_results); i++) {
pkg_res = all_results[i]
if (pkg_res.total == 0) continue
txt_report += `Package: ${pkg_res.package}\n`
for (var j = 0; j < length(pkg_res.files); j++) {
var f = pkg_res.files[j]
var status = f.failed == 0 ? "PASS" : "FAIL"
txt_report += ` [${status}] ${f.name} (${f.passed}/${length(f.tests)})\n`
txt_report = txt_report + `Package: ${pkg_res.package}\n`
for (j = 0; j < length(pkg_res.files); j++) {
f = pkg_res.files[j]
status = f.failed == 0 ? "PASS" : "FAIL"
txt_report = txt_report + ` [${status}] ${f.name} (${f.passed}/${length(f.tests)})\n`
}
}
txt_report += `\n=== FAILURES ===\n`
txt_report = txt_report + `\n=== FAILURES ===\n`
var has_failures = false
for (var i = 0; i < length(all_results); i++) {
var pkg_res = all_results[i]
for (var j = 0; j < length(pkg_res.files); j++) {
var f = pkg_res.files[j]
for (var k = 0; k < length(f.tests); k++) {
var t = f.tests[k]
for (i = 0; i < length(all_results); i++) {
pkg_res = all_results[i]
for (j = 0; j < length(pkg_res.files); j++) {
f = pkg_res.files[j]
for (k = 0; k < length(f.tests); k++) {
t = f.tests[k]
if (t.status == "failed") {
has_failures = true
txt_report += `FAIL: ${pkg_res.package} :: ${f.name} :: ${t.test}\n`
txt_report = txt_report + `FAIL: ${pkg_res.package} :: ${f.name} :: ${t.test}\n`
if (t.error) {
txt_report += ` Message: ${t.error.message}\n`
txt_report = txt_report + ` Message: ${t.error.message}\n`
if (t.error.stack) {
txt_report += ` Stack:\n${text(array(array(t.error.stack, '\n'), l => ` ${l}`), '\n')}\n`
txt_report = txt_report + ` Stack:\n${text(array(array(t.error.stack, '\n'), l => ` ${l}`), '\n')}\n`
}
}
txt_report += `\n`
txt_report = txt_report + `\n`
}
}
}
}
if (!has_failures) txt_report += `None\n`
if (!has_failures) txt_report = txt_report + `None\n`
txt_report += `\n=== DETAILED RESULTS ===\n`
for (var i = 0; i < length(all_results); i++) {
var pkg_res = all_results[i]
txt_report = txt_report + `\n=== DETAILED RESULTS ===\n`
for (i = 0; i < length(all_results); i++) {
pkg_res = all_results[i]
if (pkg_res.total == 0) continue
for (var j = 0; j < length(pkg_res.files); j++) {
var f = pkg_res.files[j]
for (var k = 0; k < length(f.tests); k++) {
var t = f.tests[k]
var dur = `${t.duration_ns || 0}ns`
var status = t.status == "passed" ? "PASS" : "FAIL"
txt_report += `[${status}] ${pkg_res.package} ${t.test} (${dur})\n`
for (j = 0; j < length(pkg_res.files); j++) {
f = pkg_res.files[j]
for (k = 0; k < length(f.tests); k++) {
t = f.tests[k]
dur = `${t.duration_ns || 0}ns`
status = t.status == "passed" ? "PASS" : "FAIL"
txt_report = txt_report + `[${status}] ${pkg_res.package} ${t.test} (${dur})\n`
}
}
}
@@ -655,19 +714,19 @@ Total: ${totals.total}, Passed: ${totals.passed}, Failed: ${totals.failed}
log.console(`Report written to ${report_dir}/test.txt`)
// Generate JSON per package
for (var i = 0; i < length(all_results); i++) {
var pkg_res = all_results[i]
for (i = 0; i < length(all_results); i++) {
pkg_res = all_results[i]
if (pkg_res.total == 0) continue
var pkg_tests = []
for (var j = 0; j < length(pkg_res.files); j++) {
var f = pkg_res.files[j]
for (var k = 0; k < length(f.tests); k++) {
pkg_tests = []
for (j = 0; j < length(pkg_res.files); j++) {
f = pkg_res.files[j]
for (k = 0; k < length(f.tests); k++) {
push(pkg_tests, f.tests[k])
}
}
var json_path = `${report_dir}/${replace(pkg_res.package, /\//, '_')}.json`
json_path = `${report_dir}/${replace(pkg_res.package, /\//, '_')}.json`
fd.slurpwrite(json_path, stone(blob(json.encode(pkg_tests))))
}
}

View File

@@ -124,14 +124,15 @@ function time_number(_rec) {
var dst = r.dst ? 1 : 0
var yday = r.yday || 0
var i = 0
if (year > time.epoch) {
var i = time.epoch
i = time.epoch
while (i < year) {
c = c + time.day * time.yearsize(i)
i = i + 1
}
} else if (year < time.epoch) {
var i = time.epoch - 1
i = time.epoch - 1
while (i > year) {
c = c + time.day * time.yearsize(i)
i = i - 1

96
toml.cm
View File

@@ -26,23 +26,33 @@ function parse_toml(toml_text) {
var current_section = result
var current_section_name = ''
for (var i = 0; i < length(lines); i++) {
var line = trim(lines[i])
var i = 0
var line = null
var inner = null
var section_path = null
var j = 0
var key = null
var eq_index = null
var key_part = null
var value = null
var unquoted = null
for (i = 0; i < length(lines); i++) {
line = trim(lines[i])
if (line == null) line = lines[i]
// Skip empty lines and comments
if (!line || starts_with(line, '#')) continue
// Section header
if (starts_with(line, '[') && ends_with(line, ']')) {
var inner = text(line, 1, -1)
var section_path = parse_key_path(inner)
inner = text(line, 1, -1)
section_path = parse_key_path(inner)
if (section_path == null) return null
current_section = result
current_section_name = text(section_path, '.')
for (var j = 0; j < length(section_path); j++) {
var key = section_path[j]
for (j = 0; j < length(section_path); j++) {
key = section_path[j]
// Only treat null as "missing"; do not clobber false/0/""
if (current_section[key] == null) {
@@ -58,18 +68,18 @@ function parse_toml(toml_text) {
}
// Key-value pair
var eq_index = search(line, '=')
eq_index = search(line, '=')
if (eq_index != null && eq_index > 0) {
var key_part = trim(text(line, 0, eq_index))
var value = trim(text(line, eq_index + 1))
key_part = trim(text(line, 0, eq_index))
value = trim(text(line, eq_index + 1))
if (key_part == null) key_part = trim(text(line, 0, eq_index))
if (value == null) value = trim(text(line, eq_index + 1))
var key = parse_key(key_part)
key = parse_key(key_part)
if (key == null) return null
if (starts_with(value, '"') && ends_with(value, '"')) {
var unquoted = text(value, 1, -1)
unquoted = text(value, 1, -1)
current_section[key] = toml_unescape(unquoted)
if (current_section[key] == null) return null
} else if (starts_with(value, '[') && ends_with(value, ']')) {
@@ -91,9 +101,9 @@ function parse_toml(toml_text) {
function parse_key(str) {
if (!is_text(str)) return null
var inner = null
if (starts_with(str, '"') && ends_with(str, '"')) {
var inner = text(str, 1, -1)
inner = text(str, 1, -1)
return toml_unescape(inner)
}
return str
@@ -107,12 +117,15 @@ function parse_key_path(str) {
var current = ''
var in_quote = false
for (var i = 0; i < length(str); i++) {
var c = str[i]
var i = 0
var c = null
var piece = null
for (i = 0; i < length(str); i++) {
c = str[i]
if (c == '"' && (i == 0 || str[i - 1] != '\\')) {
in_quote = !in_quote
} else if (c == '.' && !in_quote) {
var piece = trim(current)
piece = trim(current)
if (piece == null) piece = trim(current)
push(parts, parse_key(piece))
current = ''
@@ -140,14 +153,17 @@ function parse_array(str) {
var current = ''
var in_quotes = false
for (var i = 0; i < length(s); i++) {
var ch = s[i]
var i = 0
var ch = null
var piece = null
for (i = 0; i < length(s); i++) {
ch = s[i]
if (ch == '"' && (i == 0 || s[i - 1] != '\\')) {
in_quotes = !in_quotes
current = current + ch
} else if (ch == ',' && !in_quotes) {
var piece = trim(current)
piece = trim(current)
if (piece == null) piece = trim(current)
push(items, parse_value(piece))
current = ''
@@ -181,12 +197,14 @@ function encode_toml(obj) {
var result = []
function encode_value(value) {
var items = null
var i = 0
if (is_text(value)) return '"' + toml_escape(value) + '"'
if (is_logical(value)) return value ? 'true' : 'false'
if (is_number(value)) return text(value)
if (is_array(value)) {
var items = []
for (var i = 0; i < length(value); i++) push(items, encode_value(value[i]))
items = []
for (i = 0; i < length(value); i++) push(items, encode_value(value[i]))
return '[' + text(items, ', ') + ']'
}
return text(value)
@@ -201,29 +219,41 @@ function encode_toml(obj) {
// First pass: encode top-level simple values
var keys = array(obj)
for (var i = 0; i < length(keys); i++) {
var key = keys[i]
var value = obj[key]
var i = 0
var key = null
var value = null
for (i = 0; i < length(keys); i++) {
key = keys[i]
value = obj[key]
if (!is_object(value)) push(result, quote_key(key) + ' = ' + encode_value(value))
}
// Second pass: encode nested objects
function encode_section(o, path) {
var keys = array(o)
for (var i = 0; i < length(keys); i++) {
var key = keys[i]
var value = o[key]
var i = 0
var key = null
var value = null
var quoted = null
var section_path = null
var section_keys = null
var j = 0
var sk = null
var sv = null
for (i = 0; i < length(keys); i++) {
key = keys[i]
value = o[key]
if (is_object(value)) {
var quoted = quote_key(key)
var section_path = path ? path + '.' + quoted : quoted
quoted = quote_key(key)
section_path = path ? path + '.' + quoted : quoted
push(result, '[' + section_path + ']')
// Direct properties
var section_keys = array(value)
for (var j = 0; j < length(section_keys); j++) {
var sk = section_keys[j]
var sv = value[sk]
section_keys = array(value)
for (j = 0; j < length(section_keys); j++) {
sk = section_keys[j]
sv = value[sk]
if (!is_object(sv)) push(result, quote_key(sk) + ' = ' + encode_value(sv))
}