Synsemadocsv0.6.xENES

Lenguaje

Módulos (use / export)

Dividí el código en archivos. export hace público un task/type/let/enum/routes; todo lo demás queda privado. Un módulo es un espacio de nombres de sus exports, importado bajo un alias (keys(math) los lista).

El módulo — mathlib.syn:

export task square(n)
    give n * n

export let VERSION be "1.0"

export let STATS be {"calls": 0}

export task counted_square(n)
    set STATS["calls"] to STATS["calls"] + 1
    give n * n

task private_helper()        -- no `export` → not visible to importers
    give 42

El entry que lo importa:

modules.syn
-- Doc example: modules (use / export). Imports ./mathlib.syn under the alias `math`.
intent: "doc example: modules"
use "./mathlib.syn" as math

print("math.square(5) = " + text(math.square(5)) + ",  VERSION " + math.VERSION)

-- The message of the error `f` raises, or nothing when it does not raise.
task error_of(f)
    try
        f()
    recover err
        give err
    give nothing

test "import an exported task and let (private_helper is NOT visible)"
    assert_eq(math.square(5), 25)
    assert_eq(math.VERSION, "1.0")
    assert_eq(keys(math), ["square", "VERSION", "STATS", "counted_square"])
    assert(contains(error_of(() => math.private_helper()), "export task private_helper"))

test "an exported let is ONE variable: the module's tasks and the importer see the same one"
    let before be math.STATS
    math.counted_square(3)
    assert_eq(math.STATS["calls"], before["calls"] + 1)
    set math.STATS["calls"] to 100
    math.counted_square(3)
    assert_eq(math.STATS["calls"], 101)

test "let snap be mod.X is a snapshot (a value)"
    let snap be math.STATS
    math.counted_square(2)
    assert_eq(math.STATS["calls"], snap["calls"] + 1)

test "set mod.X rebinds the export; tasks and new names cannot be set from outside"
    set math.VERSION to "2.0"
    assert_eq(math.VERSION, "2.0")
    set math.VERSION to "1.0"
    assert(contains(error_of(() => set_task()), "cannot replace the module task 'square'"))
    assert(contains(error_of(() => set_new()), "module has no export 'NEW'"))

test "every path to the module reaches the same state; parallel workers get their own"
    let mods be {"m": math}
    set mods.m.STATS["calls"] to 7
    assert_eq(math.STATS["calls"], 7)
    let r be parallel_map((x) => math.counted_square(x), [1, 2])
    assert_eq(r, [1, 4])
    assert_eq(math.STATS["calls"], 7)

task set_task()
    set math.square to 1

task set_new()
    set math.NEW to 1

Reglas§

Estado del módulo — un let exportado es una sola variable§

Las listas y los maps son valores en todos lados, pero un módulo no es un valor: un let exportado es una sola variable, la misma que leen y escriben las tasks del propio módulo.

set math.NEW to 1 → module has no export 'NEW'; set math.square to 1 → cannot replace the module task 'square' from outside the module.

Todo camino a un módulo llega al mismo estado: dos use del mismo archivo, un re-export (export let L be math en otro módulo), o un módulo guardado en un map. Bajo serve y parallel_map cada request o worker tiene su propia copia del estado del módulo; para compartir entre ellos, usá state_*, SQL o remember.

export routes — grupos de rutas que un serve puede montar (mount)§

Un sitio grande no tiene que ser un solo bloque serve. Un módulo exporta un grupo de rutas; el entry lo monta (los cuerpos llaman a los helpers privados del módulo por nombre simple):

-- shop.syn
task fmt(n)                        -- private
    give "$" + text(n)

export routes shop
    route "GET /shop"
        give html("<h1>" + fmt(99) + "</h1>")
    route "POST /shop/buy"
        expect body {item: text}
        give created(json of request)
-- app.syn
use "./shop.syn" as shop
serve on 8080
    mount shop.shop                -- or: mount shop.shop at "/store"

Los grupos aceptan entradas route con su propio rate_limit y timeout (engine v0.6.19+: una ruta montada tiene su propia zona de rate limit, y un prefijo de mount es otra zona — antes de v0.6.19 los dos se rechazaban dentro de un grupo); las rutas stream y socket también funcionan dentro de un grupo desde engine v0.6.20 (se montan como una ruta directa, con zona de rate limit propia — antes, synsema check y serve las rechazaban). Una ruta del grupo, o el grupo entero, puede ser private (v0.6.20+: la línea private al principio del cuerpo de la ruta o del grupo): se sirve, pero queda fuera de /llms.txt, /openapi.json, /sitemap.xml y /docs — mirá Descubrimiento. Un requires auth montado sigue exigiendo auth with en el bloque serve, validado al construir el serve. Mirá Servidor HTTP y Construí un sitio web.

Módulos de confianza§

Motor v0.6.46+. Un módulo de confianza es un módulo que carga el host — escrito por una persona — y que el código en el que el host no confía (un programa escrito por un modelo, por ejemplo) sólo puede llamar. Cada task exportada corre con sus propios require, nunca con los de quien la llama. Así, un programa bajo un techo sin declassify no puede publicar los datos de un cliente, pero sí puede llamar t.reply(customer, text), cuya política desclasifica sólo la dirección del propio cliente.

-- tools.syn — written by a person, loaded by the host
export task reply(customer, body)
    require declassify("customers")
    let dest be declassify(customer.email, "the reply goes to the customer's own address")
    give {"to": dest, "length": declassify(length(body), "the length of a reply is public")}
-- app.syn — may be written by a model: under this ceiling it cannot declassify by itself
use "trusted:tools" as t
let c be private({"email": "ana@example.org", "name": "Ana"}, "customers")
print(t.reply(c, "your order shipped"))       -- {to: "ana@example.org", length: 18}
synsema run --labels --cap-set stdout --trusted tools=./tools.syn \
    --trusted-cap-set "stdout,declassify=customers" app.syn

El mismo programa con print(declassify(c.email, "direct")) se frena en esa línea: `label_violation: declassify is not granted for what this value is private to (customers) …`.

tools.syn
-- Doc example: a trusted module. The HOST loads it (`--trusted tools=./tools.syn`); the program can
-- only call it. Each exported task runs with its own `require`s, bounded by `--trusted-cap-set`.
export let config be {"limit": 3}

export task reply(customer, body)
    require declassify("customers")
    let dest be declassify(customer.email, "the reply goes to the customer's own address")
    give {"to": dest, "length": declassify(length(body), "the length of a reply is public")}

export task limit()
    give config.limit

export task shout(t)
    give upper(t)

export task apply_to(f, x)
    give f(x)
trusted.syn
-- Doc example: trusted modules. Runs ONLY as
--   synsema test --labels --cap-set stdout --trusted tools=docs/0.6.x/examples-trusted/tools.syn \
--       --trusted-cap-set "stdout,declassify=customers" docs/0.6.x/examples-trusted/trusted.syn
-- The program's own ceiling (`--cap-set stdout`) has no `declassify`: it publishes the customer's
-- address only through the tool a person wrote.
intent: "doc example: trusted modules"
use "trusted:tools" as t

let CUSTOMER be private({"email": "ana@example.org", "name": "Ana"}, "customers")

-- A program task that shadows a builtin: the tool never sees it.
task upper(x)
    give "HIJACKED"

task mine(x)
    give x

-- To offer a trusted tool to a model, wrap it in a task of the program.
task reply(customer: map, body: text)
    give t.reply(customer, body)

-- The message of the error `f` raises, or nothing when it does not raise.
task error_of(f)
    try
        f()
    recover err
        give err
    give nothing

test "the tool publishes what its policy allows"
    let r be t.reply(CUSTOMER, "your order shipped")
    assert_eq(r, {"to": "ana@example.org", "length": 18})

test "the tool never sees the program's names"
    assert_eq(t.shout("hi"), "HI")

test "arguments are data, not code"
    assert(contains(error_of(() => t.apply_to(mine, 1)), "a trusted tool takes data, not code"))
    assert(contains(error_of(() => t.apply_to((x) => x, 1)), "a trusted tool takes data, not code"))

test "the program changes only its own copy of the exports"
    set t.config to {"limit": 99}
    assert_eq(t.config, {"limit": 99})
    assert_eq(t.limit(), 3)

test "trusted tasks take positional arguments; wrap them to offer them to a model"
    assert(contains(error_of(() => t.shout(t = "x")), "does not accept named arguments"))
    assert_eq(tool_spec(reply, "Reply to a customer").params, ["customer: map", "body: text"])
    assert_eq(reply(CUSTOMER, "hi").to, "ana@example.org")

Banderas§

BanderaQué hace
--trusted <nombre>=<ruta>carga <ruta> como trusted:<nombre>; se puede repetir; en synsema run y synsema test. nombre es letras, dígitos, _ o -; la ruta es relativa al directorio de trabajo
--trusted-cap-set "<lista>"el techo de todos los módulos de confianza, con la misma sintaxis que --cap-set

En @synsema/wasm las opciones son trusted: {tools: source} y trusted_ceiling (WASM). Los errores de las banderas: `--trusted takes name=path (a name of letters, digits, '_' or '-'), e.g. --trusted tools=./tools.syn, --trusted tools: cannot read ./x.syn: …, --trusted tools: the name is given twice. synsema serve rechaza la bandera: --trusted runs under synsema run and synsema test for now (a trusted tool is not isolated in the workers of serve yet)`.

Qué puede hacer una task de confianza§

Qué puede y qué no puede hacer el programa con él§

use "trusted:tools" as t

task reply(customer: map, body: text)
    give t.reply(customer, body)

let catalog be [tool_spec(reply, "Reply to a customer")]

Un call o call_tool cuyo mapa de argumentos tiene un valor privado da un resultado privado, aunque la herramienta haya desclasificado lo que devuelve; llamá al envoltorio directo (reply(c, body)) para quedarte con el resultado público.

Lo que todavía no hace§

Cada una de estas cosas es un error adentro de una herramienta de confianza, y el mensaje dice por qué: definir o lanzar un agente, parallel_map, cron_every/cron_after, un use de un módulo de archivo — todos correrían en otro intérprete al lado de los nombres del programa — y render, con sus include y layouts (el archivo de la plantilla vive donde el programa puede escribirlo, y correría con las capacidades de la herramienta: armá el texto en la herramienta). Por ejemplo: parallel_map: a trusted tool cannot fan out yet — its workers would run with the tool's capabilities next to the program's names; loop over the items in the tool.

Con módulos de confianza el programa entero corre en el tree-walker, sin la VM de bytecode. Medido: un loop de 3 millones de iteraciones, 0,07 s → 0,8 s.