actor files now .ce; module files now .cm; add toml encoder/decoder and test

This commit is contained in:
2025-05-30 15:25:31 -05:00
parent 674eb237e0
commit 11357d4fb5
117 changed files with 376 additions and 89 deletions

8
.gitignore vendored
View File

@@ -13,20 +13,16 @@ Jenkinsfile
*.gz
*.tar
.nova/
packer*
primum
sokol-shdc*
source/shaders/*.h
core.cdb
primum.exe
core.cdb.h
jsc
.DS_Store
*.html
.vscode
*.icns
game.zip
icon.ico
steam/
subprojects/*/
build_dbg/
build_dbg/
.cell

View File

@@ -5,7 +5,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
## Build Commands
### Build variants
- `make debug` - Build debug version (uses meson debug configuration)
- `make` - Make and install debug version. Usually all that's needed.
- `make fast` - Build optimized version
- `make release` - Build release version with LTO and optimizations
- `make small` - Build minimal size version
@@ -13,10 +13,10 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
- `make crosswin` - Cross-compile for Windows using mingw32
### Testing
- `meson test -C build_dbg` - Run all tests in debug build
- `meson test -C build_<variant>` - Run tests in specific build variant
- `./build_dbg/prosperon tests/<testname>.js` - Run specific test
- Available tests: `spawn_actor`, `empty`, `nota`, `wota`, `portalspawner`, `overling`, `send`, `delay`
After install with 'make', just run 'cell' and point it at the actor you want to launch. "cell tests/toml" runs the actor "tests/toml.js"
## Scripting language
This is called "cell", but it is is a variant of javascript and extremely similar.
### Common development commands
- `meson setup build_<variant>` - Configure build directory

View File

@@ -1,6 +1,6 @@
debug: FORCE
meson setup build_dbg -Dbuildtype=debug
meson install -C build_dbg
meson install --only-changed -C build_dbg
fast: FORCE
meson setup build_fast

View File

@@ -339,18 +339,6 @@ cell_dep = declare_dependency(
link_with: cell
)
copy_tests = custom_target(
'copy_tests',
output: 'tests',
command: [
'cp', '-rf',
join_paths(meson.project_source_root(), 'tests'),
meson.project_build_root()
],
build_always_stale: true,
build_by_default: true
)
tests = [
'spawn_actor',
'empty',
@@ -363,5 +351,5 @@ tests = [
]
foreach file : tests
test(file, cell, args:['tests/' + file + '.js'], depends:copy_tests)
test(file, cell, args:['tests/' + file])
endforeach

View File

@@ -1,8 +1,10 @@
(function engine() {
globalThis.cell = prosperon
cell.hidden.console.print("HELLO\n")
cell.DOC = cell.hidden.DOCSYM
var MOD_EXT = '.cm'
var ACTOR_EXT = '.ce'
globalThis.pi = 3.1415926535897932
function caller_data(depth = 0)
@@ -22,7 +24,7 @@ function caller_data(depth = 0)
return {file,line}
}
cell.id ??= "newguy"
function console_rec(line, file, msg) {
return `[${cell.id.slice(0,5)}] [${file}:${line}]: ${msg}\n`
@@ -97,17 +99,10 @@ if (!io.exists('.cell')) {
os.exit(1);
}
io.mount("scripts")
var RESPATH = 'scripts/resources.js'
var canonical = io.realdir(RESPATH) + 'resources.js'
var content = io.slurp(RESPATH)
var resources = js.eval(RESPATH, `(function setup_resources(io){${content}})`).call({}, io)
io.mount(".cell/modules", "")
var use_cache = {}
use_cache['resources'] = resources
function print_api(obj) {
for (var prop in obj) {
if (!obj.hasOwnProperty(prop)) continue
@@ -122,7 +117,7 @@ function print_api(obj) {
var res_cache = {}
var BASEPATH = 'scripts/base.js'
var BASEPATH = 'scripts/base' + MOD_EXT
var script = io.slurp(BASEPATH)
var fnname = "base"
script = `(function ${fnname}() { ${script}; })`
@@ -132,19 +127,9 @@ var inProgress = {}
var loadingStack = []
globalThis.use = function use(file, ...args) {
// Normalize the request - remove .js extension if present
var request_name = file
if (file.endsWith('.js')) {
request_name = file.substring(0, file.length - 3)
}
// Check cache first - both 'transform' and 'transform.js' should return same cached value
for (var cached_key in use_cache) {
if (cached_key === file || cached_key === request_name ||
cached_key === request_name + '.js' ||
(cached_key.endsWith('.js') && cached_key.substring(0, cached_key.length - 3) === request_name)) {
return use_cache[cached_key]
}
// Check cache first
if (use_cache[file]) {
return use_cache[file]
}
// Check for circular dependencies
@@ -159,11 +144,13 @@ globalThis.use = function use(file, ...args) {
)
}
// Try to find the script file
var path = resources.find_script(request_name)
var path = null
if (io.exists(file + MOD_EXT) && !io.is_directory(file + MOD_EXT)) {
path = file + MOD_EXT
}
// Check if there's an embedded module
var embed_mod = use_embed(request_name)
var embed_mod = use_embed(file)
// If no script and no embedded module, error
if (!path && !embed_mod) {
@@ -173,28 +160,26 @@ globalThis.use = function use(file, ...args) {
// If only embedded module exists, return it
if (!path && embed_mod) {
use_cache[file] = embed_mod
use_cache[request_name] = embed_mod
if (file !== request_name) {
use_cache[request_name + '.js'] = embed_mod
}
return embed_mod
}
// If we have a script path, check for circular dependency
if (inProgress[path]) {
throw new Error(`Circular dependency detected while loading "${file}"`)
throw new Error(
`Circular dependency detected while loading "${file}".\n` +
`Module chain: ${loadingStack.join(" -> ")}\n` +
`Cycle specifically: ${cyclePath.join(" -> ")}`
)
}
inProgress[path] = true
loadingStack.push(file)
// Determine the compiled file path in .cell directory
var compiledPath = path + '.o'
var compiledPath = ".cell/build/" + io.realdir(path) + "/" + path + '.o'
// Ensure .cell directory exists
if (!io.exists('.cell')) {
io.mkdir('.cell')
}
io.mkdir(compiledPath.dir())
// Check if compiled version exists and is newer than source
var useCompiled = false
@@ -210,7 +195,6 @@ globalThis.use = function use(file, ...args) {
var mod_name = path.name()
if (useCompiled) {
// Load compiled bytecode
var compiledBlob = io.slurpbytes(compiledPath)
fn = js.compile_unblob(compiledBlob)
fn = js.eval_compile(fn)
@@ -245,13 +229,8 @@ globalThis.use = function use(file, ...args) {
loadingStack.pop()
delete inProgress[path]
// Cache under all possible keys
use_cache[path] = ret
// Cache the result
use_cache[file] = ret
use_cache[request_name] = ret
if (file !== request_name && !file.endsWith('.js')) {
use_cache[request_name + '.js'] = ret
}
return ret
}
@@ -286,7 +265,7 @@ stone.p = function(object)
return Object.isFrozen(object)
}
var DOCPATH = 'scripts/doc.js'
var DOCPATH = 'scripts/doc' + MOD_EXT
var script = io.slurp(DOCPATH)
var fnname = "doc"
script = `(function ${fnname}() { ${script}; })`
@@ -648,11 +627,6 @@ if (overling) actor_prep(overling, {type:'greet', actor: $_})
if (!cell.args.program)
os.exit(1)
if (typeof cell.args.program !== 'string')
cell.args.program = 'main.js';
actor_mod.setname(cell.args.program)
function destroyself() {
log.spam(`Got the message to destroy self.`)
dying = true
@@ -731,9 +705,31 @@ function enet_check()
//enet_check();
// Finally, run the program
var prog = resources.find_script(cell.args.program)
prog = io.slurp(prog)
var prog_script = `(function ${cell.args.program.name()}($_) { ${prog} })`
actor_mod.setname(cell.args.program)
var prog = null
var progPath = cell.args.program
if (io.exists(progPath + ACTOR_EXT) && !io.is_directory(progPath + ACTOR_EXT)) {
prog = progPath + ACTOR_EXT
} else if (io.exists(progPath) && io.is_directory(progPath)) {
var mainPath = progPath + '/main' + ACTOR_EXT
if (io.exists(mainPath) && !io.is_directory(mainPath)) {
prog = mainPath
}
}
if (!prog)
throw new Error(cell.args.program + " not found.");
// Mount the directory containing the program
var progDir = prog.substring(0, prog.lastIndexOf('/'))
if (progDir && progDir !== '.') {
io.mount(progDir, "")
}
var progContent = io.slurp(prog)
var prog_script = `(function ${cell.args.program.name()}($_) { ${progContent} })`
var val = js.eval(cell.args.program, prog_script)($_)
if (val)
throw new Error('Program must not return anything');

169
scripts/toml.cm Normal file
View File

@@ -0,0 +1,169 @@
// Simple TOML parser for shop.toml
// Supports basic TOML features needed for the module system
function parse_toml(text) {
var lines = text.split('\n')
var result = {}
var current_section = result
var current_section_name = ''
for (var i = 0; i < lines.length; i++) {
var line = lines[i].trim()
// Skip empty lines and comments
if (!line || line.startsWith('#')) continue
// Section header
if (line.startsWith('[') && line.endsWith(']')) {
var section_path = line.slice(1, -1).split('.')
current_section = result
current_section_name = section_path.join('.')
for (var j = 0; j < section_path.length; j++) {
var key = section_path[j]
if (!current_section[key]) {
current_section[key] = {}
}
current_section = current_section[key]
}
continue
}
// Key-value pair
var eq_index = line.indexOf('=')
if (eq_index > 0) {
var key = line.substring(0, eq_index).trim()
var value = line.substring(eq_index + 1).trim()
// Parse value
if (value.startsWith('"') && value.endsWith('"')) {
// String - unescape quotes
current_section[key] = value.slice(1, -1).replace(/\\"/g, '"')
} else if (value.startsWith('[') && value.endsWith(']')) {
// Array
current_section[key] = parse_array(value)
} else if (value === 'true' || value === 'false') {
// Boolean
current_section[key] = value === 'true'
} else if (!isNaN(Number(value))) {
// Number
current_section[key] = Number(value)
} else {
// Unquoted string
current_section[key] = value
}
}
}
return result
}
function parse_array(str) {
// Remove brackets
str = str.slice(1, -1).trim()
if (!str) return []
var items = []
var current = ''
var in_quotes = false
for (var i = 0; i < str.length; i++) {
var char = str[i]
if (char === '"' && (i === 0 || str[i-1] !== '\\')) {
in_quotes = !in_quotes
current += char
} else if (char === ',' && !in_quotes) {
items.push(parse_value(current.trim()))
current = ''
} else {
current += char
}
}
if (current.trim()) {
items.push(parse_value(current.trim()))
}
return items
}
function parse_value(str) {
if (str.startsWith('"') && str.endsWith('"')) {
return str.slice(1, -1).replace(/\\"/g, '"')
} else if (str === 'true' || str === 'false') {
return str === 'true'
} else if (!isNaN(Number(str))) {
return Number(str)
} else {
return str
}
}
function encode_toml(obj) {
var result = []
function encode_value(value) {
if (typeof value === 'string') {
return '"' + value.replace(/"/g, '\\"') + '"'
} else if (typeof value === 'boolean') {
return value ? 'true' : 'false'
} else if (typeof value === 'number') {
return String(value)
} else if (Array.isArray(value)) {
var items = []
for (var i = 0; i < value.length; i++) {
items.push(encode_value(value[i]))
}
return '[' + items.join(', ') + ']'
}
return String(value)
}
// First pass: encode top-level simple values
var keys = Object.keys(obj)
for (var i = 0; i < keys.length; i++) {
var key = keys[i]
var value = obj[key]
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
result.push(key + ' = ' + encode_value(value))
}
}
// Second pass: encode nested objects
function encode_section(obj, path) {
var keys = Object.keys(obj)
for (var i = 0; i < keys.length; i++) {
var key = keys[i]
var value = obj[key]
if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
// Nested object - create section
var section_path = path ? path + '.' + key : key
result.push('[' + section_path + ']')
// First encode direct properties of this section
var section_keys = Object.keys(value)
for (var j = 0; j < section_keys.length; j++) {
var sk = section_keys[j]
var sv = value[sk]
if (sv === null || typeof sv !== 'object' || Array.isArray(sv)) {
result.push(sk + ' = ' + encode_value(sv))
}
}
// Then encode nested sections
encode_section(value, section_path)
}
}
}
encode_section(obj, '')
return result.join('\n')
}
return {
decode: parse_toml,
encode: encode_toml
}

3
scripts/vendor.ce Normal file
View File

@@ -0,0 +1,3 @@
log.console(`not implemented.sorry.`)
$_.stop()

View File

@@ -40,7 +40,7 @@
int tracy_profiling_enabled = 0;
#define ENGINE "engine.js"
#define ENGINE "engine.cm"
static cell_rt **ready_queue = NULL;
static cell_rt **main_ready_queue = NULL;
@@ -202,7 +202,10 @@ int prosperon_mount_core(void)
eocd_pos = i;
break;
}
if (eocd_pos < 0) return fprintf(stderr, "EOCD not found\n"), free(zip_buffer_global), 0;
if (eocd_pos < 0) {
free(zip_buffer_global);
return 0;
}
uint16_t comment_length = zip_buffer_global[eocd_pos + 20] | (zip_buffer_global[eocd_pos + 21] << 8);
int eocd_size = 22 + comment_length;

View File

@@ -154,6 +154,7 @@ JSC_CCALL(os_unneeded,
JSC_CCALL(os_destroy,
cell_rt *rt = JS_GetContextOpaque(js);
rt->need_stop = 1;
set_actor_state(rt);
)
JSC_SCALL(actor_setname,

View File

@@ -10,7 +10,7 @@ function load_comment_from_api_requestor(id) {
$_.receiver(tree => {
var child_reqs = tree.children.map(child => cb => {
$_.start(e => send(e.actor, child, cb), "tests/comments.js")
$_.start(e => send(e.actor, child, cb), "tests/comments")
})
var job = parseq.par_all({

View File

@@ -45,5 +45,5 @@ $_.start(e => {
// Start status checking after a small delay
$_.delay(checkin, 0.01)
}
}, "examples/http_download_actor.js")
}, "examples/http_download_actor")

1
tests/mod1.cm Normal file
View File

@@ -0,0 +1 @@
return "ok!"

View File

@@ -10,4 +10,4 @@ $_.start(e => {
$_.stop(e.actor)
}
}, "tests/underling.js");
}, "tests/underling");

View File

@@ -31,5 +31,5 @@ $_.start(e => {
log.console(`took ${os.now()-st} secs`)
});
}, "tests/comments.js")
}, "tests/comments")

View File

@@ -1,9 +1,9 @@
// Creates a portal and a separate actor to contact
//os.createprocess(["./prosperon", "tests/portal.js"])
//os.createprocess(["./prosperon", "tests/portal"])
//$_.delay(_ => {
// os.createprocess(["./prosperon", "tests/contact.js"])
// os.createprocess(["./prosperon", "tests/contact"])
// $_.stop()
// $_.delay($_.stop, 0.1)
//}, 0.2)

Some files were not shown because too many files have changed in this diff Show More